1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.sling.launchpad.base.shared; 20 21 import java.beans.Introspector; 22 import java.io.File; 23 import java.io.FileFilter; 24 import java.io.FileOutputStream; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.OutputStream; 28 import java.net.JarURLConnection; 29 import java.net.MalformedURLException; 30 import java.net.URI; 31 import java.net.URL; 32 import java.net.URLConnection; 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.Date; 36 import java.util.List; 37 import java.util.jar.JarFile; 38 39 import org.apache.sling.commons.osgi.bundleversion.BundleVersionInfo; 40 import org.apache.sling.commons.osgi.bundleversion.FileBundleVersionInfo; 41 42 /** 43 * The <code>Loader</code> class provides utility methods for the actual 44 * launchers to help launching the framework. 45 */ 46 public class Loader { 47 48 /** 49 * The Sling home folder set by the constructor 50 */ 51 private final File slingHome; 52 53 /** 54 * Creates a loader instance to load from the given Sling home folder. 55 * Besides ensuring the existence of the Sling home folder, the constructor 56 * also removes all but the most recent launcher JAR files from the Sling 57 * home folder (thus cleaning up from previous upgrades). 58 * 59 * @param slingHome The Sling home folder. If this is <code>null</code> the 60 * default value {@link SharedConstants#SLING_HOME_DEFAULT} is 61 * assumed. 62 * @throws IllegalArgumentException If the Sling home folder exists but is 63 * not a directory or if the Sling home folder cannot be 64 * created. 65 */ 66 public Loader(final String slingHome) throws IllegalArgumentException { 67 this.slingHome = getSlingHomeFile(slingHome); 68 removeOldLauncherJars(); 69 } 70 71 /** 72 * Creates an URLClassLoader from a _launcher JAR_ file in the given 73 * slingHome directory and loads and returns the launcher class identified 74 * by the launcherClassName. 75 * 76 * @param launcherClassName The fully qualified name of a class implementing 77 * the Launcher interface. This class must have a public 78 * constructor taking no arguments. 79 * @return the Launcher instance loaded from the newly created classloader 80 * @throws NullPointerException if launcherClassName is null 81 * @throws IllegalArgumentException if the launcherClassName cannot be 82 * instantiated. The cause of the failure is contained as the 83 * cause of the exception. 84 */ 85 public Object loadLauncher(String launcherClassName) { 86 87 final File launcherJarFile = getLauncherJarFile(); 88 info("Loading launcher class " + launcherClassName + " from " + launcherJarFile.getName()); 89 if (!launcherJarFile.canRead()) { 90 throw new IllegalArgumentException("Sling Launcher JAR " 91 + launcherJarFile + " is not accessible"); 92 } 93 94 final ClassLoader loader; 95 try { 96 loader = new LauncherClassLoader(launcherJarFile); 97 } catch (MalformedURLException e) { 98 throw new IllegalArgumentException( 99 "Cannot create an URL from the Sling Launcher JAR path name", e); 100 } 101 102 try { 103 final Class<?> launcherClass = loader.loadClass(launcherClassName); 104 return launcherClass.newInstance(); 105 } catch (ClassNotFoundException cnfe) { 106 throw new IllegalArgumentException("Cannot find class " 107 + launcherClassName + " in " + launcherJarFile, cnfe); 108 } catch (InstantiationException e) { 109 throw new IllegalArgumentException( 110 "Cannot instantiate launcher class " + launcherClassName, e); 111 } catch (IllegalAccessException e) { 112 throw new IllegalArgumentException( 113 "Cannot access constructor of class " + launcherClassName, e); 114 } 115 } 116 117 /** 118 * Tries to remove as many traces of class loaded by the framework from the 119 * Java VM as possible. Most notably the following traces are removed: 120 * <ul> 121 * <li>JavaBeans property caches 122 * <li>Close the Launcher Jar File (if opened by the platform) 123 * </ul> 124 * <p> 125 * This method must be called when the notifier is called. 126 * 127 */ 128 public void cleanupVM() { 129 130 // ensure the JavaBeans introspector lets go of any classes it 131 // may haved cached after introspection 132 Introspector.flushCaches(); 133 134 // if sling home is set, check whether we have to close the 135 // launcher JAR JarFile, which might be cached in the platform 136 closeLauncherJarFile(getLauncherJarFile()); 137 } 138 139 /** 140 * Copies the contents of the launcher JAR as indicated by the URL to the 141 * sling home directory. If the existing file is is a more recent bundle version 142 * than the supplied launcher JAR file, it is is not replaced. 143 * 144 * @return <code>true</code> if the launcher JAR file has been installed or 145 * updated, <code>false</code> otherwise. 146 * @throws IOException If an error occurrs transferring the contents 147 */ 148 public boolean installLauncherJar(URL launcherJar) throws IOException { 149 final File currentLauncherJarFile = getLauncherJarFile(); 150 151 // Copy the new launcher jar to a temporary file, and 152 // extract bundle version info 153 final URLConnection launcherJarConn = launcherJar.openConnection(); 154 launcherJarConn.setUseCaches(false); 155 final File tmp = new File(slingHome, "Loader_tmp_" + System.currentTimeMillis() + SharedConstants.LAUNCHER_JAR_REL_PATH); 156 spool(launcherJarConn.getInputStream(), tmp); 157 final FileBundleVersionInfo newVi = new FileBundleVersionInfo(tmp); 158 boolean installNewLauncher = true; 159 160 try { 161 if(!newVi.isBundle()) { 162 throw new IllegalArgumentException("New launcher jar is not a bundle, cannot get version info:" + launcherJar); 163 } 164 165 // Compare versions to decide whether to use the existing or new launcher jar 166 if (currentLauncherJarFile.exists()) { 167 final FileBundleVersionInfo currentVi = new FileBundleVersionInfo(currentLauncherJarFile); 168 if(!currentVi.isBundle()) { 169 throw new IllegalArgumentException("Existing launcher jar is not a bundle, cannot get version info:" 170 + currentLauncherJarFile.getAbsolutePath()); 171 } 172 173 String info = null; 174 if(currentVi.compareTo(newVi) == 0) { 175 info = "up to date"; 176 installNewLauncher = false; 177 } else if(currentVi.compareTo(newVi) > 0) { 178 info = "more recent than ours"; 179 installNewLauncher = false; 180 } 181 182 if(info != null) { 183 info("Existing launcher is " + info + ", using it: " 184 + getBundleInfo(currentVi) + " (" + currentLauncherJarFile.getName() + ")"); 185 } 186 } 187 188 if(installNewLauncher) { 189 final File f = new File(tmp.getParentFile(), SharedConstants.LAUNCHER_JAR_REL_PATH + "." + System.currentTimeMillis()); 190 if(!tmp.renameTo(f)) { 191 throw new IllegalStateException("Failed to rename " + tmp.getName() + " to " + f.getName()); 192 } 193 info("Installing new launcher: " + launcherJar + ", " + getBundleInfo(newVi) + " (" + f.getName() + ")"); 194 } 195 } finally { 196 if(tmp.exists()) { 197 tmp.delete(); 198 } 199 } 200 201 return installNewLauncher; 202 } 203 204 /** Return relevant bundle version info for logging */ 205 static String getBundleInfo(BundleVersionInfo<?> v) { 206 final StringBuilder sb = new StringBuilder(); 207 sb.append(v.getVersion()); 208 if(v.isSnapshot()) { 209 sb.append(", Last-Modified:"); 210 sb.append(new Date(v.getBundleLastModified())); 211 } 212 return sb.toString(); 213 } 214 215 /** 216 * Removes old candidate launcher JAR files leaving the most recent one as 217 * the launcher JAR file to use on next Sling startup. 218 * 219 * @param slingHome The Sling home directory location containing the 220 * candidate launcher JAR files. 221 */ 222 private void removeOldLauncherJars() { 223 final File[] launcherJars = getLauncherJarFiles(); 224 if (launcherJars != null && launcherJars.length > 0) { 225 226 // Remove all files except current one 227 final File current = getLauncherJarFile(); 228 for(File f : launcherJars) { 229 if(f.getAbsolutePath().equals(current.getAbsolutePath())) { 230 continue; 231 } 232 String versionInfo = null; 233 try { 234 FileBundleVersionInfo vi = new FileBundleVersionInfo(f); 235 versionInfo = getBundleInfo(vi); 236 } catch(IOException ignored) { 237 } 238 info("Deleting obsolete launcher jar: " + f.getName() + ", " + versionInfo); 239 f.delete(); 240 } 241 242 // And ensure the current file has the standard launcher name 243 if (!SharedConstants.LAUNCHER_JAR_REL_PATH.equals(current.getName())) { 244 info("Renaming current launcher jar " + current.getName() 245 + " to " + SharedConstants.LAUNCHER_JAR_REL_PATH); 246 File launcherFileName = new File( 247 current.getParentFile(), 248 SharedConstants.LAUNCHER_JAR_REL_PATH); 249 current.renameTo(launcherFileName); 250 } 251 } 252 } 253 254 /** 255 * Spools the contents of the input stream to the given file replacing the 256 * contents of the file with the contents of the input stream. When this 257 * method returns, the input stream is guaranteed to be closed. 258 * 259 * @throws IOException If an error occurrs reading or writing the input 260 * stream contents. 261 */ 262 public static void spool(InputStream ins, File destFile) throws IOException { 263 OutputStream out = null; 264 try { 265 out = new FileOutputStream(destFile); 266 byte[] buf = new byte[8192]; 267 int rd; 268 while ((rd = ins.read(buf)) >= 0) { 269 out.write(buf, 0, rd); 270 } 271 } finally { 272 if (ins != null) { 273 try { 274 ins.close(); 275 } catch (IOException ignore) { 276 } 277 } 278 if (out != null) { 279 try { 280 out.close(); 281 } catch (IOException ignore) { 282 } 283 } 284 } 285 } 286 287 // ---------- internal helper 288 289 /** 290 * Returns a <code>File</code> object representing the Launcher JAR file 291 * found in the sling home folder. 292 */ 293 private File getLauncherJarFile() { 294 File result = null; 295 final File[] launcherJars = getLauncherJarFiles(); 296 if (launcherJars == null || launcherJars.length == 0) { 297 298 // return a non-existing file naming the desired primary name 299 result = new File(slingHome, 300 SharedConstants.LAUNCHER_JAR_REL_PATH); 301 302 } else { 303 // last file is the most recent one, use it 304 result = launcherJars[launcherJars.length - 1]; 305 } 306 307 return result; 308 } 309 310 /** 311 * Returns all files in the <code>slingHome</code> directory which may be 312 * considered as launcher JAR files, sorted based on their bundle version 313 * information, most recent last. These files all start with the 314 * {@link SharedConstants#LAUNCHER_JAR_REL_PATH}. This list may be empty if 315 * the launcher JAR file has not been installed yet. 316 * 317 * @param slingHome The sling home directory where the launcher JAR files 318 * are stored 319 * @return The list of candidate launcher JAR files, which may be empty. 320 * <code>null</code> is returned if an IO error occurs trying to 321 * list the files. 322 */ 323 private File[] getLauncherJarFiles() { 324 // Get list of files with names starting with our prefix 325 final File[] rawList = slingHome.listFiles(new FileFilter() { 326 public boolean accept(File pathname) { 327 return pathname.isFile() 328 && pathname.getName().startsWith( 329 SharedConstants.LAUNCHER_JAR_REL_PATH); 330 } 331 }); 332 333 // Keep only those which have valid Bundle headers, and 334 // sort them according to the bundle version numbers 335 final List<FileBundleVersionInfo> list = new ArrayList<FileBundleVersionInfo>(); 336 for(File f : rawList) { 337 FileBundleVersionInfo fvi = null; 338 try { 339 fvi = new FileBundleVersionInfo(f); 340 } catch(IOException ioe) { 341 // Cannot read bundle info from jar file - should never happen?? 342 throw new IllegalStateException("Cannot read bundle information from loader file " + f.getAbsolutePath()); 343 } 344 if(fvi.isBundle()) { 345 list.add(fvi); 346 } 347 } 348 Collections.sort(list); 349 final File [] result = new File[list.size()]; 350 int i = 0; 351 for(FileBundleVersionInfo fvi : list) { 352 result[i++] = fvi.getSource(); 353 } 354 return result; 355 } 356 357 /** 358 * Returns the <code>slingHome</code> path as a directory. If the directory 359 * does not exist it is created. If creation fails or if 360 * <code>slingHome</code> exists but is not a directory a 361 * <code>IllegalArgumentException</code> is thrown. 362 * 363 * @param slingHome The sling home directory where the launcher JAR files 364 * are stored 365 * @return The Sling home directory 366 * @throws IllegalArgumentException if <code>slingHome</code> exists and is 367 * not a directory or cannot be created as a directory. 368 */ 369 private static File getSlingHomeFile(String slingHome) { 370 if (slingHome == null) { 371 slingHome = SharedConstants.SLING_HOME_DEFAULT; 372 } 373 374 File slingDir = new File(slingHome).getAbsoluteFile(); 375 if (slingDir.exists()) { 376 if (!slingDir.isDirectory()) { 377 throw new IllegalArgumentException("Sling Home " + slingDir 378 + " exists but is not a directory"); 379 } 380 } else if (!slingDir.mkdirs()) { 381 throw new IllegalArgumentException("Sling Home " + slingDir 382 + " cannot be created as a directory"); 383 } 384 385 return slingDir; 386 } 387 388 private static void closeLauncherJarFile(final File launcherJar) { 389 try { 390 final URI launcherJarUri = launcherJar.toURI(); 391 final URL launcherJarRoot = new URL("jar:" + launcherJarUri + "!/"); 392 final URLConnection conn = launcherJarRoot.openConnection(); 393 if (conn instanceof JarURLConnection) { 394 final JarFile jarFile = ((JarURLConnection) conn).getJarFile(); 395 jarFile.close(); 396 } 397 } catch (Exception e) { 398 // better logging here 399 } 400 } 401 402 /** Meant to be overridden to display or log info */ 403 protected void info(String msg) { 404 } 405 }