blob: e28db6c2162dfe9bdb1c309134f6a02766a86f1b [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.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
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.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. */
public 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" };
/** List of watched folders */
private volatile List<WatchedFolder> watchedFolders;
/** Session shared by all WatchedFolder */
private volatile Session session;
/** The component context. */
private volatile ComponentContext componentContext;
/** Service reg for managed service. */
private volatile ServiceRegistration managedServiceRef;
/** Configuration from managed service (old pid) */
private volatile Dictionary<?, ?> oldConfiguration;
public static final String DEFAULT_NEW_CONFIG_PATH = "sling/install";
@Property(value=DEFAULT_NEW_CONFIG_PATH)
public static final String PROP_NEW_CONFIG_PATH = "sling.jcrinstall.new.config.path";
public static final String PAUSE_SCAN_NODE_PATH = "/system/sling/installer/jcr/pauseInstallation";
@Property(value= PAUSE_SCAN_NODE_PATH)
public static final String PROP_SCAN_PROP_PATH = "sling.jcrinstall.signal.path";
private volatile boolean pauseMessageLogged = false;
public static final boolean DEFAULT_ENABLE_WRITEBACK = true;
@Property(boolValue=DEFAULT_ENABLE_WRITEBACK)
public static final String PROP_ENABLE_WRITEBACK = "sling.jcrinstall.enable.writeback";
private volatile EventListener moveEventListener;
/** Convert Nodes to InstallableResources */
static interface NodeConverter {
InstallableResource convertNode(Node n, int priority)
throws RepositoryException;
}
/** 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 */
private final static AtomicInteger bgThreadCounter = new AtomicInteger();
class StoppableThread extends Thread {
/** Used for synchronizing. */
final Object lock = new Object();
volatile boolean active = true;
private final InstallerConfig cfg;
StoppableThread(final InstallerConfig cfg) {
this.cfg = cfg;
synchronized (JcrInstaller.class) {
setName("JcrInstaller." + String.valueOf(bgThreadCounter.incrementAndGet()));
}
setDaemon(true);
}
@Override
public final void run() {
logger.info("Background thread {} starting", Thread.currentThread().getName());
synchronized ( this.lock ) {
if ( this.active ) {
try {
// open session
session = repository.loginAdministrative(repository.getDefaultWorkspace());
for (String path : cfg.getRoots()) {
listeners.add(new RootFolderListener(session, cfg.getFolderNameFilter(), 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(cfg, e.getIdentifier());
JcrInstaller.this.checkChanges(cfg, 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 : cfg.getRoots()) {
findPathsToWatch(cfg, root, watchedFolders);
}
// Scan watchedFolders and register resources with installer
for(final WatchedFolder f : watchedFolders) {
f.start();
}
} 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);
}
}
}
}
if ( this.active ) {
final List<InstallableResource> resources = new LinkedList<InstallableResource>();
for(final WatchedFolder f : watchedFolders) {
if ( this.active ) {
try {
final WatchedFolder.ScanResult r = f.scan();
logger.debug("Startup: {} provides resources {}", f, r.toAdd);
resources.addAll(r.toAdd);
} catch (final RepositoryException re) {
if ( this.active ) {
logger.error("Repository exception during scanning.", re);
}
}
}
}
if ( this.active ) {
logger.debug("Registering {} resources with OSGi installer: {}", resources.size(), resources);
installer.registerResources(URL_SCHEME, resources.toArray(new InstallableResource[resources.size()]));
}
}
while (active) {
runOneCycle(cfg);
}
logger.info("Background thread {} done", Thread.currentThread().getName());
counters[RUN_LOOP_COUNTER] = -1;
}
public InstallerConfig getConfiguration() {
return this.cfg;
}
};
private volatile StoppableThread backgroundThread;
/**
* Activate this component.
*/
protected void activate(final ComponentContext context) {
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");
final InstallerConfig cfg = new InstallerConfig(logger, componentContext, oldConfiguration, settings);
backgroundThread = new StoppableThread(cfg);
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");
if ( backgroundThread != null ) {
synchronized ( backgroundThread.lock ) {
backgroundThread.active = false;
backgroundThread.lock.notify();
}
logger.debug("Waiting for " + backgroundThread.getName() + " Thread to end...");
backgroundThread = null;
}
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();
watchedFolders = null;
}
/**
* @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);
}
}
}
/** Find the paths to watch under rootPath, according to our folderNameFilter,
* and add them to result */
private void findPathsToWatch(final InstallerConfig cfg, 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(cfg, 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 InstallerConfig cfg, final Node n, final List<WatchedFolder> result) throws RepositoryException {
final String path = n.getPath();
final int priority = cfg.getFolderNameFilter().getPriority(path);
if (priority > 0) {
result.add(new WatchedFolder(session, path, priority, cfg.getConverters()));
}
final int depth = path.split("/").length;
if(depth > cfg.getMaxWatchedFolderDepth()) {
logger.debug("Not recursing into {} due to maxWatchedFolderDepth={}", path, cfg.getMaxWatchedFolderDepth());
return;
}
final NodeIterator it = n.getNodes();
while (it.hasNext()) {
findPathsUnderNode(cfg, 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(final InstallerConfig cfg) throws Exception {
logger.debug("Updating folder list.");
final List<String> result = new LinkedList<String>();
final List<WatchedFolder> newFolders = new ArrayList<WatchedFolder>();
for(String root : cfg.getRoots()) {
findPathsToWatch(cfg, 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 InstallerConfig cfg, final String path) {
for(String root : cfg.getRoots()) {
if (path.startsWith(root)) {
logger.info("Got event for root {}, scheduling scanning of new folders", root);
updateFoldersListTimer.scheduleScan();
}
}
}
InstallerConfig getConfiguration() {
InstallerConfig cfg = null;
final StoppableThread st = this.backgroundThread;
if ( st != null ) {
cfg = st.getConfiguration();
}
return cfg;
}
/**
* @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
// get configuration
final InstallerConfig cfg = this.getConfiguration();
if ( cfg != null ) {
try {
while(it.hasNext()) {
final Event e = it.nextEvent();
logger.debug("Got event {}", e);
this.checkChanges(cfg, 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(final InstallerConfig cfg) {
logger.debug("Running watch cycle.");
try {
boolean didRefresh = false;
if (anyWatchFolderNeedsScan()) {
session.refresh(false);
didRefresh = true;
if (scanningIsPaused(cfg)) {
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", cfg.getPauseScanNodePath());
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(cfg);
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);
}
synchronized ( backgroundThread.lock ) {
try {
backgroundThread.lock.wait(RUN_LOOP_DELAY_MSEC);
} catch (final InterruptedException ignore) {
Thread.currentThread().interrupt();
}
}
counters[RUN_LOOP_COUNTER]++;
}
boolean scanningIsPaused(final InstallerConfig cfg) throws RepositoryException {
if (session.nodeExists(cfg.getPauseScanNodePath())) {
Node node = session.getNode(cfg.getPauseScanNodePath());
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, cfg.getPauseScanNodePath());
}
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) {
// get configuration
final InstallerConfig cfg = this.getConfiguration();
if ( cfg == null || !cfg.isWriteBack() ) {
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 = cfg.getFolderNameFilter().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 ( cfg.getFolderNameFilter().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 InstallerConfig cfg, final String oldPath) {
final String path;
// check root path, we use the path with highest prio
final String rootPath = cfg.getFolderNameFilter().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) {
// get configuration
final InstallerConfig cfg = this.getConfiguration();
if ( cfg == null || !cfg.isWriteBack() ) {
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(cfg, 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(cfg, cfg.getNewConfigPath() + 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 = cfg.getNewConfigPath() + name + ".config";
logger.debug("Add of {} at {}", resourceType, path);
}
// write to a byte array stream
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("# Configuration created by Apache Sling JCR Installer\n".getBytes("UTF-8"));
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(cfg.getFolderNameFilter().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();
}
}
}
}