blob: 3077258d81f01642876ba96d452da395a8f07eae [file] [log] [blame]
/*
* 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.installer.provider.jcr.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import org.apache.felix.cm.file.ConfigurationHandler;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.PropertyUnbounded;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.OsgiInstaller;
import org.apache.sling.installer.api.UpdateHandler;
import org.apache.sling.installer.api.UpdateResult;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main class of jcrinstall, runs as a service, observes the
* repository for changes in folders having names that match
* configurable regular expressions, and registers resources
* found in those folders with the OSGi installer for installation.
*/
@Component(label="%jcrinstall.name", description="%jcrinstall.description", immediate=true, metatype=true)
@Properties({
@Property(name="service.description", value="Sling JCR Install Service"),
@Property(name="service.vendor", value="The Apache Software Foundation"),
@Property(name=UpdateHandler.PROPERTY_SCHEMES, value=JcrInstaller.URL_SCHEME, unbounded=PropertyUnbounded.ARRAY),
@Property(name="service.ranking", intValue=100)
})
@Service(value=UpdateHandler.class)
public class JcrInstaller implements EventListener, UpdateHandler, ManagedService {
public static final long RUN_LOOP_DELAY_MSEC = 500L;
public static final String URL_SCHEME = "jcrinstall";
/** PID before refactoring. */
private static final String OLD_PID = "org.apache.sling.jcr.install.impl.JcrInstaller";
private final Logger logger = LoggerFactory.getLogger(getClass());
/** Counters, used for statistics and testing */
private final long [] counters = new long[COUNTERS_COUNT];
public static final int SCAN_FOLDERS_COUNTER = 0;
public static final int UPDATE_FOLDERS_LIST_COUNTER = 1;
public static final int RUN_LOOP_COUNTER = 2;
public static final int COUNTERS_COUNT = 3;
private static final String NT_FILE = "nt:file";
private static final String NT_RESOURCE = "nt:resource";
private static final String PROP_DATA = "jcr:data";
private static final String PROP_MODIFIED = "jcr:lastModified";
private static final String PROP_ENC = "jcr:encoding";
private static final String PROP_MIME = "jcr:mimeType";
private static final String MIME_TXT = "text/plain";
private static final String ENCODING = "UTF-8";
/**
* This class watches the repository for installable resources
*/
@Reference
private SlingRepository repository;
/**
* Additional installation folders are activated based
* on the current RunMode. For example, /libs/foo/install.dev
* if the current run mode is "dev".
*/
@Reference
private SlingSettingsService settings;
/**
* The OsgiInstaller installs resources in the OSGi framework.
*/
@Reference
private OsgiInstaller installer;
/** Default regexp for watched folders */
public static final String DEFAULT_FOLDER_NAME_REGEXP = ".*/install$";
/**
* ComponentContext property that overrides the folder name regexp
*/
@Property(value=DEFAULT_FOLDER_NAME_REGEXP)
public static final String FOLDER_NAME_REGEXP_PROPERTY = "sling.jcrinstall.folder.name.regexp";
public static final int DEFAULT_FOLDER_MAX_DEPTH = 4;
/**
* Configurable max. path depth for watched folders
*/
@Property(intValue=DEFAULT_FOLDER_MAX_DEPTH)
public static final String PROP_INSTALL_FOLDER_MAX_DEPTH = "sling.jcrinstall.folder.max.depth";
/**
* Configurable search path, with per-path priorities.
* We could get it from the ResourceResolver, but introducing a dependency on this just to get those
* values is too much for this module that's meant to bootstrap other services.
*/
@Property(value={"/libs:100", "/apps:200"}, unbounded=PropertyUnbounded.ARRAY)
public static final String PROP_SEARCH_PATH = "sling.jcrinstall.search.path";
public static final String [] DEFAULT_SEARCH_PATH = { "/libs:100", "/apps:200" };
private int maxWatchedFolderDepth;
/** Filter for folder names */
private FolderNameFilter folderNameFilter;
/** List of watched folders */
private List<WatchedFolder> watchedFolders;
/** Session shared by all WatchedFolder */
private Session session;
/** The root folders that we watch */
private String [] roots;
/** The component context. */
private ComponentContext componentContext;
/** Service reg for managed service. */
private ServiceRegistration managedServiceRef;
/** Configuration from managed service (old pid) */
private Dictionary<?, ?> oldConfiguration;
private static final String DEFAULT_NEW_CONFIG_PATH = "sling/install";
@Property(value=DEFAULT_NEW_CONFIG_PATH)
private static final String PROP_NEW_CONFIG_PATH = "sling.jcrinstall.new.config.path";
/** The path for new configurations. */
private String newConfigPath;
public static final String PAUSE_SCAN_NODE_PATH = "/system/sling/installer/jcr/pauseInstallation";
@Property(value= PAUSE_SCAN_NODE_PATH)
private static final String PROP_SCAN_PROP_PATH = "sling.jcrinstall.signal.path";
/** The path for pauseInstallation property */
private String pauseScanNodePath;
private volatile boolean pauseMessageLogged = false;
private static final boolean DEFAULT_ENABLE_WRITEBACK = true;
@Property(boolValue=DEFAULT_ENABLE_WRITEBACK)
private static final String PROP_ENABLE_WRITEBACK = "sling.jcrinstall.enable.writeback";
/** Write back enabled? */
private boolean writeBack;
private EventListener moveEventListener;
/** Convert Nodes to InstallableResources */
static interface NodeConverter {
InstallableResource convertNode(Node n, int priority)
throws RepositoryException;
}
/** Our NodeConverters*/
private final Collection <NodeConverter> converters = new ArrayList<NodeConverter>();
/** Detect newly created folders that we must watch */
private final List<RootFolderListener> listeners = new LinkedList<RootFolderListener>();
/** Timer used to call updateFoldersList() */
private final RescanTimer updateFoldersListTimer = new RescanTimer();
/** Thread that can be cleanly stopped with a flag */
static int bgThreadCounter;
class StoppableThread extends Thread {
boolean active = true;
StoppableThread() {
synchronized (JcrInstaller.class) {
setName("JcrInstaller." + (++bgThreadCounter));
}
setDaemon(true);
}
@Override
public final void run() {
logger.info("Background thread {} starting", Thread.currentThread().getName());
try {
// open session
session = repository.loginAdministrative(repository.getDefaultWorkspace());
for (String path : roots) {
listeners.add(new RootFolderListener(session, folderNameFilter, path, updateFoldersListTimer));
logger.debug("Configured root folder: {}", path);
}
// Watch for events on the root - that might be one of our root folders
session.getWorkspace().getObservationManager().addEventListener(JcrInstaller.this,
Event.NODE_ADDED | Event.NODE_REMOVED,
"/",
false, // isDeep
null,
null,
true); // noLocal
// add special observation listener for move events
JcrInstaller.this.moveEventListener = new EventListener() {
/**
* @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
*/
public void onEvent(final EventIterator events) {
try {
while (events.hasNext()) {
final Event e = events.nextEvent();
JcrInstaller.this.checkChanges(e.getIdentifier());
JcrInstaller.this.checkChanges(e.getPath());
}
} catch (final RepositoryException re) {
logger.warn("RepositoryException in onEvent", re);
}
}
};
session.getWorkspace().getObservationManager().addEventListener(
moveEventListener,
Event.NODE_MOVED,
"/",
true, // isDeep
null,
null,
true); // noLocal
logger.debug("Watching for node events on / to detect removal/add of our root folders");
// Find paths to watch and create WatchedFolders to manage them
watchedFolders = new LinkedList<WatchedFolder>();
for(String root : roots) {
findPathsToWatch(root, watchedFolders);
}
// Scan watchedFolders and register resources with installer
final List<InstallableResource> resources = new LinkedList<InstallableResource>();
for(WatchedFolder f : watchedFolders) {
f.start();
final WatchedFolder.ScanResult r = f.scan();
logger.debug("Startup: {} provides resources {}", f, r.toAdd);
resources.addAll(r.toAdd);
}
logger.debug("Registering {} resources with OSGi installer: {}", resources.size(), resources);
installer.registerResources(URL_SCHEME, resources.toArray(new InstallableResource[resources.size()]));
} catch (final RepositoryException re) {
logger.error("Repository exception during startup - deactivating installer!", re);
active = false;
final ComponentContext ctx = componentContext;
if ( ctx != null ) {
final String name = (String) componentContext.getProperties().get(
ComponentConstants.COMPONENT_NAME);
ctx.disableComponent(name);
}
}
while (active) {
runOneCycle();
}
logger.info("Background thread {} done", Thread.currentThread().getName());
counters[RUN_LOOP_COUNTER] = -1;
}
};
private StoppableThread backgroundThread;
/**
* Activate this component.
*/
protected void activate(final ComponentContext context) {
if (backgroundThread != null) {
throw new IllegalStateException("Expected backgroundThread to be null in activate()");
}
this.componentContext = context;
this.start();
final Dictionary<String, Object> props = new Hashtable<String, Object>();
props.put(Constants.SERVICE_PID, OLD_PID);
this.managedServiceRef = this.componentContext.getBundleContext().registerService(ManagedService.class.getName(),
this,
props);
}
private void start() {
logger.info("Activating Apache Sling JCR Installer");
this.writeBack = PropertiesUtil.toBoolean(getPropertyValue(PROP_ENABLE_WRITEBACK), DEFAULT_ENABLE_WRITEBACK);
// Setup converters
converters.add(new FileNodeConverter());
converters.add(new ConfigNodeConverter());
// Configurable max depth, system property (via bundle context) overrides default value
final Object obj = getPropertyValue(PROP_INSTALL_FOLDER_MAX_DEPTH);
if (obj != null) {
// depending on where it's coming from, obj might be a string or integer
maxWatchedFolderDepth = Integer.valueOf(String.valueOf(obj)).intValue();
logger.debug("Using configured ({}) folder name max depth '{}'", PROP_INSTALL_FOLDER_MAX_DEPTH, maxWatchedFolderDepth);
} else {
maxWatchedFolderDepth = DEFAULT_FOLDER_MAX_DEPTH;
logger.debug("Using default folder max depth {}, not provided by {}", maxWatchedFolderDepth, PROP_INSTALL_FOLDER_MAX_DEPTH);
}
// Configurable folder regexp, system property overrides default value
String folderNameRegexp = (String)getPropertyValue(FOLDER_NAME_REGEXP_PROPERTY);
if(folderNameRegexp != null) {
folderNameRegexp = folderNameRegexp.trim();
logger.debug("Using configured ({}) folder name regexp '{}'", FOLDER_NAME_REGEXP_PROPERTY, folderNameRegexp);
} else {
folderNameRegexp = DEFAULT_FOLDER_NAME_REGEXP;
logger.debug("Using default folder name regexp '{}', not provided by {}", folderNameRegexp, FOLDER_NAME_REGEXP_PROPERTY);
}
// Setup folder filtering and watching
folderNameFilter = new FolderNameFilter(PropertiesUtil.toStringArray(getPropertyValue(PROP_SEARCH_PATH), DEFAULT_SEARCH_PATH),
folderNameRegexp, settings.getRunModes());
roots = folderNameFilter.getRootPaths();
// setup default path for new configurations
this.newConfigPath = PropertiesUtil.toString(getPropertyValue(PROP_NEW_CONFIG_PATH), DEFAULT_NEW_CONFIG_PATH);
final boolean postSlash = newConfigPath.endsWith("/");
if ( !postSlash ) {
this.newConfigPath = newConfigPath.concat("/");
}
final boolean preSlash = newConfigPath.startsWith("/");
if ( !preSlash ) {
this.newConfigPath = this.folderNameFilter.getRootPaths()[0] + '/' + this.newConfigPath;
}
this.pauseScanNodePath = PropertiesUtil.toString(getPropertyValue(PROP_SCAN_PROP_PATH), PAUSE_SCAN_NODE_PATH);
backgroundThread = new StoppableThread();
backgroundThread.start();
}
/**
* Deactivate this component
*/
protected void deactivate(final ComponentContext context) {
if ( this.managedServiceRef != null ) {
this.managedServiceRef.unregister();
this.managedServiceRef = null;
}
this.stop();
this.componentContext = null;
}
private void stop() {
logger.info("Deactivating Apache Sling JCR Installer");
final long timeout = 30000L;
backgroundThread.active = false;
logger.debug("Waiting for " + backgroundThread.getName() + " Thread to end...");
backgroundThread.interrupt();
try {
backgroundThread.join(timeout);
} catch(InterruptedException iex) {
// ignore this as we want to shutdown
}
backgroundThread = null;
folderNameFilter = null;
watchedFolders = null;
converters.clear();
try {
if (session != null) {
for(RootFolderListener wfc : listeners) {
wfc.cleanup(session);
}
session.getWorkspace().getObservationManager().removeEventListener(this);
if ( moveEventListener != null ) {
session.getWorkspace().getObservationManager().removeEventListener(moveEventListener);
moveEventListener = null;
}
}
} catch (final RepositoryException e) {
logger.warn("Exception in stop()", e);
}
if ( session != null ) {
session.logout();
session = null;
}
listeners.clear();
}
/**
* @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary)
*/
public void updated(@SuppressWarnings("rawtypes") Dictionary properties)
throws ConfigurationException {
final boolean restart;
if ( this.oldConfiguration == null ) {
restart = properties != null;
} else {
restart = true;
}
this.oldConfiguration = properties;
if ( restart ) {
try {
this.stop();
this.start();
} catch (final Exception e) {
logger.error("Error restarting", e);
}
}
}
/** Get a property value from the old config, component context or bundle context */
protected Object getPropertyValue(final String name) {
final Dictionary<?, ?> oldConfig = this.oldConfiguration;
Object result = null;
if ( oldConfig != null ) {
result = oldConfig.get(name);
if ( result != null ) {
logger.warn("Using configuration value from obsolete configuration with PID {} for property {}." +
" Please merge this configuration into the configuration with the PID {}.",
new Object[] {OLD_PID, name, this.componentContext.getProperties().get(Constants.SERVICE_PID)});
}
}
if ( result == null ) {
result = this.componentContext.getBundleContext().getProperty(name);
if (result == null) {
result = this.componentContext.getProperties().get(name);
}
}
return result;
}
/** Find the paths to watch under rootPath, according to our folderNameFilter,
* and add them to result */
private void findPathsToWatch(final String rootPath, final List<WatchedFolder> result) throws RepositoryException {
Session s = null;
try {
s = repository.loginAdministrative(repository.getDefaultWorkspace());
if (!s.itemExists(rootPath) || !s.getItem(rootPath).isNode() ) {
logger.info("Bundles root node {} not found, ignored", rootPath);
} else {
logger.debug("Bundles root node {} found, looking for bundle folders inside it", rootPath);
final Node n = (Node)s.getItem(rootPath);
findPathsUnderNode(n, result);
}
} finally {
if (s != null) {
s.logout();
}
}
}
/**
* Add n to result if it is a folder that we must watch, and recurse into its children
* to do the same.
*/
void findPathsUnderNode(final Node n, final List<WatchedFolder> result) throws RepositoryException {
final String path = n.getPath();
final int priority = folderNameFilter.getPriority(path);
if (priority > 0) {
result.add(new WatchedFolder(session, path, priority, converters));
}
final int depth = path.split("/").length;
if(depth > maxWatchedFolderDepth) {
logger.debug("Not recursing into {} due to maxWatchedFolderDepth={}", path, maxWatchedFolderDepth);
return;
}
final NodeIterator it = n.getNodes();
while (it.hasNext()) {
findPathsUnderNode(it.nextNode(), result);
}
}
/**
* Add WatchedFolder to our list if it doesn't exist yet.
*/
private void addWatchedFolder(final WatchedFolder toAdd)
throws RepositoryException {
WatchedFolder existing = null;
for(WatchedFolder wf : watchedFolders) {
if (wf.getPath().equals(toAdd.getPath())) {
existing = wf;
break;
}
}
if (existing == null) {
toAdd.start();
watchedFolders.add(toAdd);
}
}
/** Add new folders to watch if any have been detected
* @return a list of InstallableResource that must be unregistered,
* for folders that have been removed
*/
private List<String> updateFoldersList() throws Exception {
logger.debug("Updating folder list.");
final List<String> result = new LinkedList<String>();
final List<WatchedFolder> newFolders = new ArrayList<WatchedFolder>();
for(String root : roots) {
findPathsToWatch(root, newFolders);
}
for(WatchedFolder wf : newFolders) {
addWatchedFolder(wf);
}
// Check all WatchedFolder, in case some were deleted
final List<WatchedFolder> toRemove = new ArrayList<WatchedFolder>();
for(WatchedFolder wf : watchedFolders) {
logger.debug("Item {} exists? {}", wf.getPath(), session.itemExists(wf.getPath()));
if(!session.itemExists(wf.getPath())) {
result.addAll(wf.scan().toRemove);
wf.stop();
toRemove.add(wf);
}
}
for(final WatchedFolder wf : toRemove) {
logger.info("Deleting {}, path does not exist anymore", wf);
watchedFolders.remove(wf);
}
return result;
}
/**
* Check for changes in any of the root folders
*/
private void checkChanges(final String path) {
for(String root : roots) {
if (path.startsWith(root)) {
logger.info("Got event for root {}, scheduling scanning of new folders", root);
updateFoldersListTimer.scheduleScan();
}
}
}
/**
* @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
*/
public void onEvent(final EventIterator it) {
// Got a DELETE or ADD on root - schedule folders rescan if one
// of our root folders is impacted
try {
while(it.hasNext()) {
final Event e = it.nextEvent();
logger.debug("Got event {}", e);
this.checkChanges(e.getPath());
}
} catch(RepositoryException re) {
logger.warn("RepositoryException in onEvent", re);
}
}
/**
* Run periodic scans of our watched folders, and watch for folders creations/deletions.
*/
public void runOneCycle() {
logger.debug("Running watch cycle.");
try {
boolean didRefresh = false;
if (anyWatchFolderNeedsScan()) {
session.refresh(false);
didRefresh = true;
if (scanningIsPaused()) {
if (!pauseMessageLogged) {
//Avoid flooding the logs every 500 msec so log at info level once
logger.info("Detected signal for pausing the JCR Provider i.e. child nodes found under path {}. " +
"JCR Provider scanning would not be performed", pauseScanNodePath);
pauseMessageLogged = true;
}
return;
} else if (pauseMessageLogged) {
pauseMessageLogged = false;
}
}
// Rescan WatchedFolders if needed
boolean scanWf = false;
for(WatchedFolder wf : watchedFolders) {
if (!wf.needsScan()) {
continue;
}
scanWf = true;
if ( !didRefresh ) {
session.refresh(false);
didRefresh = true;
}
counters[SCAN_FOLDERS_COUNTER]++;
final WatchedFolder.ScanResult sr = wf.scan();
boolean toDo = false;
if ( sr.toAdd.size() > 0 ) {
logger.info("Registering resource with OSGi installer: {}",sr.toAdd);
toDo = true;
}
if ( sr.toRemove.size() > 0 ) {
logger.info("Removing resource from OSGi installer: {}", sr.toRemove);
toDo = true;
}
if ( toDo ) {
installer.updateResources(URL_SCHEME, sr.toAdd.toArray(new InstallableResource[sr.toAdd.size()]),
sr.toRemove.toArray(new String[sr.toRemove.size()]));
}
}
// Update list of WatchedFolder if we got any relevant events,
// or if there were any WatchedFolder events
if (scanWf || updateFoldersListTimer.expired()) {
if (!didRefresh) {
session.refresh(false);
didRefresh = true;
}
updateFoldersListTimer.reset();
counters[UPDATE_FOLDERS_LIST_COUNTER]++;
final List<String> toRemove = updateFoldersList();
if ( toRemove.size() > 0 ) {
logger.info("Removing resource from OSGi installer (folder deleted): {}", toRemove);
installer.updateResources(URL_SCHEME, null,
toRemove.toArray(new String[toRemove.size()]));
}
}
} catch (final Exception e) {
logger.warn("Exception in runOneCycle()", e);
}
try {
Thread.sleep(RUN_LOOP_DELAY_MSEC);
} catch (final InterruptedException ignore) {
// ignore
}
counters[RUN_LOOP_COUNTER]++;
}
boolean scanningIsPaused() throws RepositoryException {
if (session.nodeExists(pauseScanNodePath)) {
Node node = session.getNode(pauseScanNodePath);
boolean result = node.hasNodes();
if (result && logger.isDebugEnabled()) {
List<String> nodeNames = new ArrayList<String>();
NodeIterator childItr = node.getNodes();
while (childItr.hasNext()) {
nodeNames.add(childItr.nextNode().getName());
}
logger.debug("Found child nodes {} at path {}. Scanning would be paused", nodeNames, pauseScanNodePath);
}
return result;
}
return false;
}
private boolean anyWatchFolderNeedsScan() {
for (WatchedFolder wf : watchedFolders) {
if (wf.needsScan()) {
return true;
}
}
return false;
}
long [] getCounters() {
return counters;
}
/**
* @see org.apache.sling.installer.api.UpdateHandler#handleRemoval(java.lang.String, java.lang.String, java.lang.String)
*/
public UpdateResult handleRemoval(final String resourceType,
final String id,
final String url) {
if ( !this.writeBack ) {
return null;
}
final int pos = url.indexOf(':');
final String path = url.substring(pos + 1);
// check path (SLING-2407)
// 0. Check protocol
if ( !url.startsWith(URL_SCHEME) ) {
logger.debug("Not removing unmanaged artifact from repository: {}", url);
return null;
}
// 1. Is this a system configuration then don't delete
final String[] rootPaths = this.folderNameFilter.getRootPaths();
final String systemConfigRootPath = rootPaths[rootPaths.length - 1];
if ( path.startsWith(systemConfigRootPath) ) {
logger.debug("Not removing system artifact from repository at {}", path);
return null;
}
// 2. Is this configuration provisioned by us
boolean found = false;
int lastSlash = path.lastIndexOf('/');
while (!found && lastSlash > 1) {
final String prefix = path.substring(0, lastSlash);
if ( this.folderNameFilter.getPriority(prefix) != -1 ) {
found = true;
} else {
lastSlash = prefix.lastIndexOf('/');
}
}
if ( found ) {
// remove
logger.debug("Removing artifact at {}", path);
Session session = null;
try {
session = this.repository.loginAdministrative(null);
if ( session.itemExists(path) ) {
session.getItem(path).remove();
session.save();
}
} catch (final RepositoryException re) {
logger.error("Unable to remove resource from " + path, re);
return null;
} finally {
if ( session != null ) {
session.logout();
}
}
return new UpdateResult(url);
}
// not provisioned by us
logger.debug("Not removing unmanaged artifact from repository at {}", path);
return null;
}
/**
* @see org.apache.sling.installer.api.UpdateHandler#handleUpdate(java.lang.String, java.lang.String, java.lang.String, java.util.Dictionary, Map)
*/
public UpdateResult handleUpdate(final String resourceType,
final String id,
final String url,
final Dictionary<String, Object> dict,
final Map<String, Object> attributes) {
return this.handleUpdate(resourceType, id, url, null, dict, attributes);
}
/**
* @see org.apache.sling.installer.api.UpdateHandler#handleUpdate(java.lang.String, java.lang.String, java.lang.String, java.io.InputStream, Map)
*/
public UpdateResult handleUpdate(final String resourceType,
final String id,
final String url,
final InputStream is,
final Map<String, Object> attributes) {
return this.handleUpdate(resourceType, id, url, is, null, attributes);
}
private String getPathWithHighestPrio(final String oldPath) {
final String path;
// check root path, we use the path with highest prio
final String rootPath = this.folderNameFilter.getRootPaths()[0] + '/';
if ( !oldPath.startsWith(rootPath) ) {
final int slashPos = oldPath.indexOf('/', 1);
path = rootPath + oldPath.substring(slashPos + 1);
} else {
path = oldPath;
}
return path;
}
/**
* Internal implementation of update handling
*/
private UpdateResult handleUpdate(final String resourceType,
final String id,
final String url,
final InputStream is,
final Dictionary<String, Object> dict,
final Map<String, Object> attributes) {
if ( !this.writeBack ) {
return null;
}
// we only handle add/update of configs for now
if ( !resourceType.equals(InstallableResource.TYPE_CONFIG) ) {
return null;
}
Session session = null;
try {
session = this.repository.loginAdministrative(null);
final String path;
boolean resourceIsMoved = true;
if ( url != null ) {
// update
final int pos = url.indexOf(':');
final String oldPath = url.substring(pos + 1);
// calculate the new node path
final String nodePath;
if ( url.startsWith(URL_SCHEME + ':') ) {
nodePath = getPathWithHighestPrio(oldPath);
} else {
final int lastSlash = url.lastIndexOf('/');
final int lastPos = url.lastIndexOf('.');
final String name;
if ( lastSlash == -1 || lastPos < lastSlash ) {
name = id;
} else {
name = url.substring(lastSlash + 1, lastPos);
}
nodePath = getPathWithHighestPrio(this.newConfigPath + name + ".config");
}
// ensure extension 'config'
if ( !nodePath.endsWith(".config") ) {
if ( session.itemExists(nodePath) ) {
session.getItem(nodePath).remove();
}
path = nodePath + ".config";
} else {
path = nodePath;
}
resourceIsMoved = nodePath.equals(oldPath);
logger.debug("Update of {} at {}", resourceType, path);
} else {
// add
final String name;
if ( attributes != null && attributes.get(InstallableResource.RESOURCE_URI_HINT) != null ) {
name = (String)attributes.get(InstallableResource.RESOURCE_URI_HINT);
} else {
name = id;
}
path = this.newConfigPath + name + ".config";
logger.debug("Add of {} at {}", resourceType, path);
}
// write to a byte array stream
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
ConfigurationHandler.write(baos, dict);
baos.close();
// get or create file node
JcrUtil.createPath(session, path, NT_FILE);
// get or create resource node
final Node dataNode = JcrUtil.createPath(session, path + "/jcr:content", NT_RESOURCE);
dataNode.setProperty(PROP_DATA, new ByteArrayInputStream(baos.toByteArray()));
dataNode.setProperty(PROP_MODIFIED, Calendar.getInstance());
dataNode.setProperty(PROP_ENC, ENCODING);
dataNode.setProperty(PROP_MIME, MIME_TXT);
session.save();
final UpdateResult result = new UpdateResult(JcrInstaller.URL_SCHEME + ':' + path);
// priority
final int lastSlash = path.lastIndexOf('/');
final String parentPath = path.substring(0, lastSlash);
result.setPriority(this.folderNameFilter.getPriority(parentPath));
result.setResourceIsMoved(resourceIsMoved);
return result;
} catch (final RepositoryException re) {
logger.error("Unable to add/update resource " + resourceType + ':' + id, re);
return null;
} catch (final IOException e) {
logger.error("Unable to add/update resource " + resourceType + ':' + id, e);
return null;
} finally {
if ( session != null ) {
session.logout();
}
}
}
}