blob: 36e8067ebf1f3f0525a9f597df1427b8bbc59e57 [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.factories.configuration.impl;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.OsgiInstaller;
import org.apache.sling.installer.api.ResourceChangeListener;
import org.apache.sling.installer.api.event.InstallationEvent;
import org.apache.sling.installer.api.event.InstallationListener;
import org.apache.sling.installer.api.info.InfoProvider;
import org.apache.sling.installer.api.info.Resource;
import org.apache.sling.installer.api.info.ResourceGroup;
import org.apache.sling.installer.api.tasks.ChangeStateTask;
import org.apache.sling.installer.api.tasks.InstallTask;
import org.apache.sling.installer.api.tasks.InstallTaskFactory;
import org.apache.sling.installer.api.tasks.RegisteredResource;
import org.apache.sling.installer.api.tasks.ResourceState;
import org.apache.sling.installer.api.tasks.ResourceTransformer;
import org.apache.sling.installer.api.tasks.TaskResource;
import org.apache.sling.installer.api.tasks.TaskResourceGroup;
import org.apache.sling.installer.api.tasks.TransformationResult;
import org.apache.sling.installer.factories.configuration.ConfigurationConstants;
import org.osgi.framework.Constants;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Task creator for configurations.
*/
public class ConfigTaskCreator
implements InstallTaskFactory, ConfigurationListener, ResourceTransformer, InstallationListener {
/** Logger. */
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** Configuration admin. */
private final ConfigurationAdmin configAdmin;
/** Resource change listener. */
private final ResourceChangeListener changeListener;
/** Resource change listener. */
private final OsgiInstaller installer;
/** Resource change listener. */
private final InfoProvider infoProvider;
public ConfigTaskCreator(final ResourceChangeListener listener,
final ConfigurationAdmin configAdmin,
final OsgiInstaller installer,
final InfoProvider infoProvider) {
this.changeListener = listener;
this.configAdmin = configAdmin;
this.installer = installer;
this.infoProvider = infoProvider;
}
/**
* Create a task to install or uninstall a configuration.
*
* @see org.apache.sling.installer.api.tasks.InstallTaskFactory#createTask(org.apache.sling.installer.api.tasks.TaskResourceGroup)
*/
@Override
public InstallTask createTask(final TaskResourceGroup group) {
final TaskResource toActivate = group.getActiveResource();
if ( !toActivate.getType().equals(InstallableResource.TYPE_CONFIG) ) {
return null;
}
final InstallTask result;
if (toActivate.getState() == ResourceState.UNINSTALL) {
// if this is an uninstall, check if we have to install an older version
// in this case we should do an update instead of uninstall/install (!)
final TaskResource second = group.getNextActiveResource();
if ( second != null
&& ( second.getState() == ResourceState.IGNORED || second.getState() == ResourceState.INSTALLED || second.getState() == ResourceState.INSTALL )
&& ( second.getDictionary() == null || second.getDictionary().get(InstallableResource.RESOURCE_IS_TEMPLATE) == null)) {
result = new ChangeStateTask(group, ResourceState.UNINSTALLED);
} else {
result = new ConfigRemoveTask(group, this.configAdmin);
}
} else {
result = new ConfigInstallTask(group, this.configAdmin);
}
return result;
}
/**
* @see org.osgi.service.cm.ConfigurationListener#configurationEvent(org.osgi.service.cm.ConfigurationEvent)
*/
@Override
public void configurationEvent(final ConfigurationEvent event) {
Configuration updateConfig = null;
ResourceGroup updateGroup = null;
synchronized ( Coordinator.SHARED ) {
if ( event.getType() == ConfigurationEvent.CM_DELETED ) {
final Coordinator.Operation op = Coordinator.SHARED.get(event.getPid(), event.getFactoryPid(), true);
if ( op == null ) {
updateGroup = this.convertOldInstallerResource(event);
if ( updateGroup == null ) {
this.changeListener.resourceRemoved(InstallableResource.TYPE_CONFIG, event.getPid());
}
} else {
this.logger.debug("Ignoring configuration event for {}:{}", event.getPid(), event.getFactoryPid());
}
} else if ( event.getType() == ConfigurationEvent.CM_UPDATED ) {
try {
// we just need to pass in the pid as we're using named factory configs
final Configuration config = ConfigUtil.getConfiguration(configAdmin,
null,
event.getPid());
final Coordinator.Operation op = Coordinator.SHARED.get(event.getPid(), event.getFactoryPid(), false);
if ( config != null && op == null ) {
final boolean persist = ConfigUtil.toBoolean(config.getProperties().get(ConfigurationConstants.PROPERTY_PERSISTENCE), true);
final Dictionary<String, Object> dict = ConfigUtil.cleanConfiguration(config.getProperties());
final Map<String, Object> attrs = new HashMap<>();
if ( !persist ) {
attrs.put(ResourceChangeListener.RESOURCE_PERSIST, Boolean.FALSE);
}
attrs.put(Constants.SERVICE_PID, event.getPid());
attrs.put(InstallableResource.RESOURCE_URI_HINT, event.getPid());
if ( config.getBundleLocation() != null ) {
attrs.put(InstallableResource.INSTALLATION_HINT, config.getBundleLocation());
}
// Factory?
if (event.getFactoryPid() != null) {
attrs.put(ConfigurationAdmin.SERVICE_FACTORYPID, event.getFactoryPid());
}
updateGroup = this.convertOldInstallerResource(event);
if ( updateGroup == null ) {
this.changeListener.resourceAddedOrUpdated(InstallableResource.TYPE_CONFIG, event.getPid(), null, dict, attrs);
} else {
updateConfig = config;
}
} else {
this.logger.debug("Ignoring configuration event for {}:{}", event.getPid(), event.getFactoryPid());
}
} catch ( final Exception ignore) {
// ignore for now
}
}
}
if ( updateGroup != null ) {
// update installer state from old config to new config
// store config dictionary for update
final Dictionary<String, Object> dict = updateConfig == null ? null : ConfigUtil.cleanConfiguration(updateConfig.getProperties());
try {
String name = null;
for(final Resource r : updateGroup.getResources()) {
if ( name == null ) {
name = (String)r.getAttribute(Constants.SERVICE_PID);
}
final int pos = r.getURL().indexOf(':');
final String rsrcPath = r.getURL().substring(pos +1);
this.startListener(r.getURL(), 2);
final Dictionary<String, Object> newProps = ConfigUtil.cleanConfiguration(r.getDictionary());
newProps.put(this.getClass().getName(), "true");
final InstallableResource ins1 = new InstallableResource(rsrcPath, null, newProps, r.getDigest(), r.getType(), r.getPriority());
this.installer.updateResources(r.getScheme(), new InstallableResource[] {ins1}, null);
this.waitForInstall();
this.startListener(r.getURL(), 1);
newProps.remove(this.getClass().getName());
final InstallableResource ins2 = new InstallableResource(rsrcPath, null, newProps, r.getDigest(), r.getType(), r.getPriority());
this.installer.updateResources(r.getScheme(), new InstallableResource[] {ins2}, null);
this.waitForInstall();
}
if ( updateConfig != null ) {
// then delete old config
final Coordinator.Operation op = new Coordinator.Operation(event.getPid(), event.getFactoryPid(), true);
Coordinator.SHARED.add(op);
updateConfig.delete();
// finally re-create new config
final Configuration newConfig = this.configAdmin.getFactoryConfiguration(event.getFactoryPid(), name, null);
newConfig.update(dict);
} else {
// delete newly created config
final Configuration newConfig = this.configAdmin.getFactoryConfiguration(event.getFactoryPid(), name, null);
newConfig.delete();
}
} catch ( final Exception ignore) {
logger.error("An error occured while updating factory configuration " + event.getFactoryPid() + "-" + event.getPid(), ignore);
}
}
}
private ResourceGroup convertOldInstallerResource(final ConfigurationEvent event) {
if ( event.getFactoryPid() != null && !event.getPid().contains("~") ) {
// check if this configuration is processed by the installer
final String id = event.getFactoryPid() + '.' + (event.getPid().startsWith(event.getFactoryPid() + '.') ?
event.getPid().substring(event.getFactoryPid().length() + 1) : event.getPid());
final List<ResourceGroup> groups = this.infoProvider.getInstallationState().getInstalledResources();
for(final ResourceGroup grp : groups) {
if ( grp.getAlias() != null ) {
final String alias;
if ( grp.getAlias().startsWith(event.getFactoryPid() + ".") ) {
alias = grp.getAlias().substring(event.getFactoryPid().length() + 1);
} else {
alias = grp.getAlias();
}
for(final Resource rsrc : grp.getResources()) {
if ( InstallableResource.TYPE_CONFIG.equals(rsrc.getType()) && id.equals(alias) ) {
// we need to update the installer state
return grp;
}
}
}
}
}
return null;
}
/**
* @see org.apache.sling.installer.api.tasks.ResourceTransformer#transform(org.apache.sling.installer.api.tasks.RegisteredResource)
*/
@Override
public TransformationResult[] transform(final RegisteredResource resource) {
if ( resource.getType().equals(InstallableResource.TYPE_PROPERTIES) ) {
return checkConfiguration(resource);
}
return null;
}
private static String getResourceId(final String rawUrl) {
final String url = separatorsToUnix(rawUrl);
int pos = url.lastIndexOf('/');
if ( pos == -1 ) {
pos = url.indexOf(':');
}
final String lastIdPart;
if ( pos != -1 ) {
lastIdPart = url.substring(pos + 1);
} else {
lastIdPart = url;
}
return lastIdPart;
}
/**
* Check if the registered resource is a configuration
* @param resource The resource
*/
private TransformationResult[] checkConfiguration(final RegisteredResource resource) {
final String lastIdPart = getResourceId(resource.getURL());
// remove extension if known
final String pid = removeConfigExtension(lastIdPart);
// split pid and factory pid alias
final Map<String, Object> attr = new HashMap<>();
final String factoryPid;
final String configPid;
int n = pid.indexOf('~');
if ( n == -1 ) {
n = pid.indexOf('-');
}
if (n > 0) {
configPid = pid.substring(n + 1);
factoryPid = pid.substring(0, n);
attr.put(ConfigurationAdmin.SERVICE_FACTORYPID, factoryPid);
} else {
factoryPid = null;
configPid = pid;
}
attr.put(Constants.SERVICE_PID, configPid);
final TransformationResult tr = new TransformationResult();
final String id = (factoryPid == null ? configPid : ConfigUtil.getPIDOfFactoryPID(factoryPid, configPid));
tr.setId(id);
tr.setResourceType(InstallableResource.TYPE_CONFIG);
tr.setAttributes(attr);
return new TransformationResult[] {tr};
}
private volatile CountDownLatch latch;
private volatile String waitingForUrl;
@Override
public void onEvent(final InstallationEvent event) {
if ( waitingForUrl != null ) {
if ( event.getType() == InstallationEvent.TYPE.PROCESSED ) {
final TaskResource rsrc = (TaskResource) event.getSource();
if ( rsrc.getURL().equals(waitingForUrl) ) {
latch.countDown();
}
}
}
}
private void startListener(final String url, final int count) {
this.latch = new CountDownLatch(count);
this.waitingForUrl = url;
}
public void waitForInstall() {
try {
this.latch.await(50, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
this.waitingForUrl = null;
this.latch = null;
}
private static final List<String> EXTENSIONS = Arrays.asList(".config", ".properties", ".cfg", ".cfg.json");
private static String removeConfigExtension(final String id) {
for(final String ext : EXTENSIONS) {
if ( id.endsWith(ext) ) {
return id.substring(0, id.length() - ext.length());
}
}
return id;
}
/**
* Converts all separators to the Unix separator of forward slash.
*
* @param path the path to be changed, null ignored
* @return the updated path
*/
private static String separatorsToUnix(final String path) {
if (path == null || path.indexOf('\\') == -1) {
return path;
}
return path.replace('\\', '/');
}
}