| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.sling.launchpad.base.impl; |
| |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.Map; |
| |
| import org.apache.felix.framework.Logger; |
| import org.apache.sling.launchpad.api.LaunchpadContentProvider; |
| import org.apache.sling.launchpad.api.StartupMode; |
| import org.apache.sling.launchpad.base.shared.SharedConstants; |
| import org.osgi.framework.Constants; |
| |
| /** |
| * The <code>StartupManager</code> tries to detect the startup mode: |
| * It distinguishes between an initial startup (INSTALL), an update (UPDATE) |
| * and a restart without a change (RESTART). |
| * @since 2.4.0 |
| */ |
| public class StartupManager { |
| |
| /** The data file which works as a marker to detect the first startup. */ |
| private static final String DATA_FILE = "launchpad-timestamp.txt"; |
| |
| /** The old data file. */ |
| private static final String OLD_DATA_FILE = "bundle0" + File.separatorChar + "bootstrapinstaller.ser"; |
| |
| /** Name of the mode override property. */ |
| private static final String OVERRIDE_PROP = "org.apache.sling.launchpad.startupmode"; |
| |
| /** |
| * The {@link Logger} use for logging messages during installation and |
| * startup. |
| */ |
| private final Logger logger; |
| |
| private final StartupMode mode; |
| |
| private final File startupDir; |
| |
| private final File confDir; |
| |
| private final long targetStartLevel; |
| |
| private final boolean incrementalStartupEnabled; |
| |
| StartupManager(final Map<String, String> properties, |
| final Logger logger) { |
| this.logger = logger; |
| this.startupDir = DirectoryUtil.getStartupDir(properties); |
| this.confDir = DirectoryUtil.getConfigDir(properties); |
| // check for override property |
| final String overrideMode = System.getProperty(OVERRIDE_PROP, properties.get(OVERRIDE_PROP)); |
| if ( overrideMode != null ) { |
| this.mode = StartupMode.valueOf(overrideMode.toUpperCase()); |
| this.logger.log(Logger.LOG_INFO, "Override property set. Starting in mode " + this.mode); |
| } else { |
| this.mode = detectMode(properties.get(Constants.FRAMEWORK_STORAGE)); |
| this.logger.log(Logger.LOG_INFO, "Detected startup mode. Starting in mode " + this.mode); |
| } |
| |
| // populate the sling target start level from the framework one, if not set, |
| // otherwise overwrite the framework one |
| if (null == properties.get(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL)) { |
| properties.put(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL, properties.get(Constants.FRAMEWORK_BEGINNING_STARTLEVEL)); |
| } else { |
| properties.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, properties.get(SharedConstants.SLING_INSTALL_TARGETSTARTLEVEL)); |
| } |
| |
| this.targetStartLevel = Long.valueOf(properties.get(Constants.FRAMEWORK_BEGINNING_STARTLEVEL)); |
| |
| this.incrementalStartupEnabled = Boolean.valueOf(properties.get(SharedConstants.SLING_INSTALL_INCREMENTAL_START)); |
| |
| // if this is not a restart, reduce start level |
| if ( this.mode != StartupMode.RESTART && this.incrementalStartupEnabled ) { |
| final String startLevel = properties.get(SharedConstants.SLING_INSTALL_STARTLEVEL); |
| properties.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, startLevel != null ? startLevel : "10"); |
| } |
| } |
| |
| /** |
| * Return the startup mode |
| * @return The startup mode |
| */ |
| public StartupMode getMode() { |
| return this.mode; |
| } |
| |
| /** |
| * Is the incremental startup enabled? |
| */ |
| public boolean isIncrementalStartupEnabled() { |
| return this.incrementalStartupEnabled; |
| } |
| |
| /** |
| * Return the target start level. |
| * @return Target start level |
| */ |
| public long getTargetStartLevel() { |
| return this.targetStartLevel; |
| } |
| |
| /** |
| * Detect the startup mode by comparing time stamps |
| */ |
| private StartupMode detectMode(final String osgiStorageDir) { |
| final File dataFile = new File(this.confDir, DATA_FILE); |
| if (dataFile.exists()) { |
| |
| FileReader fis = null; |
| try { |
| final long selfStamp = this.getSelfTimestamp(); |
| if (selfStamp > 0) { |
| |
| fis = new FileReader(dataFile); |
| final char[] txt = new char[128]; |
| final int len = fis.read(txt); |
| final String value = new String(txt, 0, len); |
| |
| final long storedStamp = Long.parseLong(value); |
| |
| logger.log(Logger.LOG_INFO, String.format("Stored startup timestamp: %s", storedStamp)); |
| |
| return (storedStamp >= selfStamp ? StartupMode.RESTART : StartupMode.UPDATE); |
| } |
| |
| } catch (final NumberFormatException nfe) { |
| // probably still the old value, fallback to assume not |
| // installed |
| return StartupMode.RESTART; |
| |
| } catch (final IOException ioe) { |
| logger.log(Logger.LOG_ERROR, |
| "IOException during reading of installed flag.", ioe); |
| |
| } finally { |
| if (fis != null) { |
| try { fis.close(); } catch (IOException ignore) {} |
| } |
| } |
| } else { |
| // check for old data file |
| // this is a little bit hacky as we have to directly look into |
| // the Apache Felix bundle cache. However, as we know that older |
| // versions did use Felix this is fine. |
| final File felixDir = new File(osgiStorageDir); |
| final File oldFile = new File(felixDir, OLD_DATA_FILE); |
| if ( oldFile.exists() ) { |
| // this is an update - remove old file |
| oldFile.delete(); |
| return StartupMode.UPDATE; |
| } |
| } |
| // not installed yet - fallback |
| return StartupMode.INSTALL; |
| } |
| |
| /** |
| * Get the time stamp of a class through its url classloader (if possible) |
| */ |
| long getTimeStampOfClass(final Class<?> clazz, final long selfStamp) { |
| long timeStamp = selfStamp; |
| final ClassLoader loader = clazz.getClassLoader(); |
| if (loader instanceof URLClassLoader) { |
| @SuppressWarnings("resource") |
| final URLClassLoader urlLoader = (URLClassLoader) loader; |
| final URL[] urls = urlLoader.getURLs(); |
| if (urls.length > 0) { |
| final URL url = urls[0]; |
| try { |
| final long stamp = urls[0].openConnection().getLastModified(); |
| if ( stamp > selfStamp ) { |
| logger.log(Logger.LOG_INFO, String.format("Newer timestamp for %s from %s : %s", clazz.getName(), url, selfStamp)); |
| timeStamp = stamp; |
| } |
| } catch (final IOException ignore) {} |
| } |
| } |
| return timeStamp; |
| } |
| |
| /** |
| * Returns the time stamp of JAR file from which this class has been loaded |
| * or -1 if the timestamp cannot be resolved. |
| * <p> |
| * This method assumes that the ClassLoader of this class is an |
| * URLClassLoader and that the first URL entry of this class loader is the |
| * JAR providing this class. This is in fact true as the URLClassLoader has |
| * been created by the launcher from the launcher JAR file. |
| * |
| * @return The last modification time stamp of the launcher JAR file or -1 |
| * if the class loader of this class is not an URLClassLoader or the |
| * class loader has no URL entries. Both situations are not really |
| * expected. |
| * @throws IOException If an error occurs reading accessing the last |
| * modification time stamp. |
| */ |
| private long getSelfTimestamp() { |
| |
| // the time stamp of the launcher jar and the bootstrap jar |
| long selfStamp = this.getTimeStampOfClass(this.getClass(), -1); |
| selfStamp = this.getTimeStampOfClass(LaunchpadContentProvider.class, selfStamp); |
| |
| // check whether any bundle is younger than the launcher jar |
| final File[] directories = this.startupDir.listFiles(DirectoryUtil.DIRECTORY_FILTER); |
| if ( directories != null ) { |
| for (final File levelDir : directories) { |
| |
| // iterate through all files in the startlevel dir |
| final File[] jarFiles = levelDir.listFiles(DirectoryUtil.BUNDLE_FILE_FILTER); |
| if ( jarFiles != null ) { |
| for (final File bundleJar : jarFiles) { |
| if (bundleJar.lastModified() > selfStamp) { |
| selfStamp = bundleJar.lastModified(); |
| logger.log(Logger.LOG_INFO, String.format("Newer timestamp from %s : %s", bundleJar, selfStamp)); |
| } |
| } |
| } |
| } |
| } |
| |
| logger.log(Logger.LOG_INFO, String.format("Final self timestamp: %s.", selfStamp)); |
| |
| // return the final stamp (may be -1 if launcher jar cannot be checked |
| // and there are no bundle jar files) |
| return selfStamp; |
| } |
| |
| /** |
| * Set the finished installation marker. |
| */ |
| public void markInstalled() { |
| final File dataFile = new File(this.confDir, DATA_FILE); |
| try { |
| this.confDir.mkdirs(); |
| final FileWriter fos = new FileWriter(dataFile); |
| try { |
| fos.write(String.valueOf(System.currentTimeMillis())); |
| } finally { |
| try { fos.close(); } catch (final IOException ignore) {} |
| } |
| } catch (final IOException ioe) { |
| logger.log(Logger.LOG_ERROR, |
| "IOException during writing of installed flag.", ioe); |
| } |
| } |
| } |