Java tutorial
/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.module.launcher; import static org.mule.module.launcher.DefaultArchiveDeployer.ARTIFACT_NAME_PROPERTY; import static org.mule.module.launcher.DefaultArchiveDeployer.ZIP_FILE_SUFFIX; import static org.mule.util.SplashScreen.miniSplash; import org.mule.config.StartupContext; import org.mule.module.launcher.application.Application; import org.mule.module.launcher.artifact.Artifact; import org.mule.module.launcher.domain.Domain; import org.mule.module.launcher.util.DebuggableReentrantLock; import org.mule.module.launcher.util.ElementAddedEvent; import org.mule.module.launcher.util.ElementRemovedEvent; import org.mule.module.launcher.util.ObservableList; import org.mule.util.ArrayUtils; import org.mule.util.CollectionUtils; import org.mule.util.StringUtils; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate; import org.apache.commons.collections.Predicate; import org.apache.commons.io.filefilter.AndFileFilter; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.filefilter.FileFileFilter; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * It's in charge of the whole deployment process. * <p/> * It will deploy the applications at the container startup process. * It will periodically scan the artifact directories in order to process new deployments, * remove artifacts that were previously deployed but the anchor file was removed and redeploy * those applications which configuration has changed. */ public class DeploymentDirectoryWatcher implements Runnable { public static final String ARTIFACT_ANCHOR_SUFFIX = "-anchor.txt"; public static final String CHANGE_CHECK_INTERVAL_PROPERTY = "mule.launcher.changeCheckInterval"; public static final IOFileFilter ZIP_ARTIFACT_FILTER = new AndFileFilter(new SuffixFileFilter(ZIP_FILE_SUFFIX), FileFileFilter.FILE); protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000; protected transient final Log logger = LogFactory.getLog(getClass()); private final ReentrantLock deploymentLock; private final ArchiveDeployer<Domain> domainArchiveDeployer; private final ArchiveDeployer<Application> applicationArchiveDeployer; private final ArtifactTimestampListener<Application> applicationTimestampListener; private final ArtifactTimestampListener<Domain> domainTimestampListener; private final ObservableList<Application> applications; private final ObservableList<Domain> domains; private final File appsDir; private final File domainsDir; private ScheduledExecutorService artifactDirMonitorTimer; protected volatile boolean dirty; public DeploymentDirectoryWatcher(final ArchiveDeployer<Domain> domainArchiveDeployer, final ArchiveDeployer<Application> applicationArchiveDeployer, ObservableList<Domain> domains, ObservableList<Application> applications, final ReentrantLock deploymentLock) { this.appsDir = applicationArchiveDeployer.getDeploymentDirectory(); this.domainsDir = domainArchiveDeployer.getDeploymentDirectory(); this.deploymentLock = deploymentLock; this.domainArchiveDeployer = domainArchiveDeployer; this.applicationArchiveDeployer = applicationArchiveDeployer; this.applications = applications; this.domains = domains; applications.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent) { if (logger.isDebugEnabled()) { logger.debug("Deployed applications set has been modified, flushing state."); } dirty = true; } } }); domains.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent) { if (logger.isDebugEnabled()) { logger.debug("Deployed applications set has been modified, flushing state."); } dirty = true; } } }); this.applicationTimestampListener = new ArtifactTimestampListener(applications); this.domainTimestampListener = new ArtifactTimestampListener(domains); } /** * Starts the process of deployment / undeployment of artifact. * <p/> * It wil schedule a task for periodically scan the deployment directories. */ public void start() { deploymentLock.lock(); deleteAllAnchors(); // mule -app app1:app2:app3 will restrict deployment only to those specified apps final Map<String, Object> options = StartupContext.get().getStartupOptions(); String appString = (String) options.get("app"); try { String[] explodedDomains = domainsDir.list(DirectoryFileFilter.DIRECTORY); String[] packagedDomains = domainsDir.list(ZIP_ARTIFACT_FILTER); deployPackedDomains(packagedDomains); deployExplodedDomains(explodedDomains); if (appString == null) { String[] explodedApps = appsDir.list(DirectoryFileFilter.DIRECTORY); String[] packagedApps = appsDir.list(ZIP_ARTIFACT_FILTER); deployPackedApps(packagedApps); deployExplodedApps(explodedApps); } else { String[] apps = appString.split(":"); apps = removeDuplicateAppNames(apps); for (String app : apps) { try { File applicationFile = new File(appsDir, app + ZIP_FILE_SUFFIX); if (applicationFile.exists() && applicationFile.isFile()) { applicationArchiveDeployer.deployPackagedArtifact(app + ZIP_FILE_SUFFIX); } else { applicationArchiveDeployer.deployExplodedArtifact(app); } } catch (Exception e) { // Ignore and continue } } } } finally { if (deploymentLock.isHeldByCurrentThread()) { deploymentLock.unlock(); } } // only start the monitor thread if we launched in default mode without explicitly // stated applications to launch if (!(appString != null)) { scheduleChangeMonitor(); } else { if (logger.isInfoEnabled()) { logger.info(miniSplash("Mule is up and running in a fixed app set mode")); } } } /** * Stops the deployment scan service. */ public void stop() { stopAppDirMonitorTimer(); deploymentLock.lock(); try { stopArtifacts(applications); stopArtifacts(domains); } finally { deploymentLock.unlock(); } } private void stopArtifacts(List<? extends Artifact> artifacts) { Collections.reverse(artifacts); for (Artifact artifact : artifacts) { try { artifact.stop(); artifact.dispose(); } catch (Throwable t) { logger.error(t); } } } private static int getChangesCheckIntervalMs() { try { String value = System.getProperty(CHANGE_CHECK_INTERVAL_PROPERTY); return Integer.parseInt(value); } catch (NumberFormatException e) { return DEFAULT_CHANGES_CHECK_INTERVAL_MS; } } private void scheduleChangeMonitor() { final int reloadIntervalMs = getChangesCheckIntervalMs(); artifactDirMonitorTimer = Executors .newSingleThreadScheduledExecutor(new ArtifactDeployerMonitorThreadFactory()); artifactDirMonitorTimer.scheduleWithFixedDelay(this, 0, reloadIntervalMs, TimeUnit.MILLISECONDS); if (logger.isInfoEnabled()) { logger.info(miniSplash(String.format("Mule is up and kicking (every %dms)", reloadIntervalMs))); } } private void deployPackedApps(String[] zips) { for (String zip : zips) { try { applicationArchiveDeployer.deployPackagedArtifact(zip); } catch (Exception e) { // Ignore and continue } } } private void deployExplodedApps(String[] apps) { for (String addedApp : apps) { try { applicationArchiveDeployer.deployExplodedArtifact(addedApp); } catch (DeploymentException e) { // Ignore and continue } } } // Cycle is: // undeployArtifact removed apps // undeployArtifact removed domains // deploy domain archives // deploy domain exploded // redeploy modified apps // deploy archives apps // deploy exploded apps public void run() { try { if (logger.isDebugEnabled()) { logger.debug("Checking for changes..."); } // use non-barging lock to preserve fairness, according to javadocs // if there's a lock present - wait for next poll to do anything if (!deploymentLock.tryLock(0, TimeUnit.SECONDS)) { if (logger.isDebugEnabled()) { logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " + ((DebuggableReentrantLock) deploymentLock).getOwner()); } return; } undeployRemovedApps(); undeployRemovedDomains(); // list new apps String[] domains = domainsDir.list(DirectoryFileFilter.DIRECTORY); final String[] domainZips = domainsDir.list(ZIP_ARTIFACT_FILTER); redeployModifiedDomains(); deployPackedDomains(domainZips); // re-scan exploded domains and update our state, as deploying Mule domains archives might have added some if (domainZips.length > 0 || dirty) { domains = domainsDir.list(DirectoryFileFilter.DIRECTORY); } deployExplodedDomains(domains); redeployModifiedApplications(); // list new apps String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY); final String[] appZips = appsDir.list(ZIP_ARTIFACT_FILTER); deployPackedApps(appZips); // re-scan exploded apps and update our state, as deploying Mule app archives might have added some if (appZips.length > 0 || dirty) { apps = appsDir.list(DirectoryFileFilter.DIRECTORY); } deployExplodedApps(apps); } catch (Exception e) { // preserve the flag for the thread Thread.currentThread().interrupt(); } finally { if (deploymentLock.isHeldByCurrentThread()) { deploymentLock.unlock(); } dirty = false; } } public <T extends Artifact> T findArtifact(String artifactName, ObservableList<T> artifacts) { return (T) CollectionUtils.find(artifacts, new BeanPropertyValueEqualsPredicate(ARTIFACT_NAME_PROPERTY, artifactName)); } private void undeployRemovedDomains() { undeployRemovedArtifacts(domainsDir, domains, domainArchiveDeployer); } private void undeployRemovedApps() { undeployRemovedArtifacts(appsDir, applications, applicationArchiveDeployer); } private void undeployRemovedArtifacts(File artifactDir, ObservableList<? extends Artifact> artifacts, ArchiveDeployer<? extends Artifact> archiveDeployer) { // we care only about removed anchors String[] currentAnchors = artifactDir.list(new SuffixFileFilter(ARTIFACT_ANCHOR_SUFFIX)); if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append(String.format("Current anchors:%n")); for (String currentAnchor : currentAnchors) { sb.append(String.format(" %s%n", currentAnchor)); } logger.debug(sb.toString()); } String[] artifactAnchors = findExpectedAnchorFiles(artifacts); @SuppressWarnings("unchecked") final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(artifactAnchors), Arrays.asList(currentAnchors)); if (logger.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); sb.append(String.format("Deleted anchors:%n")); for (String deletedAnchor : deletedAnchors) { sb.append(String.format(" %s%n", deletedAnchor)); } logger.debug(sb.toString()); } for (String deletedAnchor : deletedAnchors) { String artifactName = StringUtils.removeEnd(deletedAnchor, ARTIFACT_ANCHOR_SUFFIX); try { if (findArtifact(artifactName, artifacts) != null) { archiveDeployer.undeployArtifact(artifactName); } else if (logger.isDebugEnabled()) { logger.debug(String.format("Artifact [%s] has already been undeployed via API", artifactName)); } } catch (Throwable t) { logger.error("Failed to undeployArtifact artifact: " + artifactName, t); } } } /** * Returns the list of anchor file names for the deployed apps * * @return a non null list of file names */ private String[] findExpectedAnchorFiles(ObservableList<? extends Artifact> artifacts) { String[] anchors = new String[artifacts.size()]; int i = 0; for (Artifact artifact : artifacts) { anchors[i++] = artifact.getArtifactName() + ARTIFACT_ANCHOR_SUFFIX; } return anchors; } private void deployExplodedDomains(String[] domains) { for (String addedApp : domains) { try { domainArchiveDeployer.deployExplodedArtifact(addedApp); } catch (DeploymentException e) { // Ignore and continue } } } private void deployPackedDomains(String[] zips) { for (String zip : zips) { try { domainArchiveDeployer.deployPackagedArtifact(zip); } catch (Exception e) { // Ignore and continue } } } private void deleteAllAnchors() { deleteAnchorsFromDirectory(domainsDir); deleteAnchorsFromDirectory(appsDir); } private void deleteAnchorsFromDirectory(final File directory) { // Deletes any leftover anchor files from previous shutdowns String[] anchors = directory.list(new SuffixFileFilter(ARTIFACT_ANCHOR_SUFFIX)); for (String anchor : anchors) { // ignore result new File(directory, anchor).delete(); } } private String[] removeDuplicateAppNames(String[] apps) { List<String> appNames = new LinkedList<String>(); for (String appName : apps) { if (!appNames.contains(appName)) { appNames.add(appName); } } return appNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY); } private void redeployModifiedDomains() { redeployModifiedArtifacts(domains, domainTimestampListener, domainArchiveDeployer); } private void redeployModifiedApplications() { Collection redeployableApplications = CollectionUtils.select(applications, new Predicate() { @Override public boolean evaluate(Object object) { return ((Application) object).getDescriptor().isRedeploymentEnabled(); } }); redeployModifiedArtifacts(redeployableApplications, applicationTimestampListener, applicationArchiveDeployer); } private <T extends Artifact> void redeployModifiedArtifacts(Collection<T> artifacts, ArtifactTimestampListener<T> artifactTimestampListener, ArchiveDeployer<T> artifactArchiveDeployer) { for (T artifact : artifacts) { if (artifactTimestampListener.isArtifactResourceUpdated(artifact)) { try { artifactArchiveDeployer.redeploy(artifact); } catch (DeploymentException e) { if (logger.isDebugEnabled()) { logger.debug(e); } } } } } private void stopAppDirMonitorTimer() { if (artifactDirMonitorTimer != null) { artifactDirMonitorTimer.shutdown(); try { artifactDirMonitorTimer.awaitTermination(getChangesCheckIntervalMs(), TimeUnit.MILLISECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } } } private static class ArtifactTimestampListener<T extends Artifact> implements PropertyChangeListener { private Map<String, ArtifactResourcesTimestamp<T>> artifactConfigResourcesTimestaps = new HashMap<String, ArtifactResourcesTimestamp<T>>(); public ArtifactTimestampListener(ObservableList<T> artifacts) { artifacts.addPropertyChangeListener(this); } @Override public void propertyChange(PropertyChangeEvent event) { if (event instanceof ElementAddedEvent) { Artifact artifactAdded = (T) event.getNewValue(); artifactConfigResourcesTimestaps.put(artifactAdded.getArtifactName(), new ArtifactResourcesTimestamp<T>(artifactAdded)); } else if (event instanceof ElementRemovedEvent) { Artifact artifactRemoved = (T) event.getNewValue(); artifactConfigResourcesTimestaps.remove(artifactRemoved.getArtifactName()); } } public boolean isArtifactResourceUpdated(T artifact) { ArtifactResourcesTimestamp<T> applicationResourcesTimestamp = artifactConfigResourcesTimestaps .get(artifact.getArtifactName()); return !applicationResourcesTimestamp.resourcesHaveSameTimestamp(artifact); } } private static class ArtifactResourcesTimestamp<T extends Artifact> { private final Map<String, Long> timestampsPerResource = new HashMap<String, Long>(); public ArtifactResourcesTimestamp(final Artifact artifact) { for (File configResourceFile : artifact.getResourceFiles()) { timestampsPerResource.put(configResourceFile.getAbsolutePath(), configResourceFile.lastModified()); } } public boolean resourcesHaveSameTimestamp(final T artifact) { boolean resourcesHaveSameTimestamp = true; for (File configResourceFile : artifact.getResourceFiles()) { long originalTimestamp = timestampsPerResource.get(configResourceFile.getAbsolutePath()); long currentTimestamp = configResourceFile.lastModified(); if (originalTimestamp != currentTimestamp) { timestampsPerResource.put(configResourceFile.getAbsolutePath(), currentTimestamp); resourcesHaveSameTimestamp = false; } } return resourcesHaveSameTimestamp; } } }