blob: ba681b397e42bd2b5b62f0bdee7d0dbbd44dd187 [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.rewriter.impl;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.rewriter.PipelineConfiguration;
import org.apache.sling.rewriter.ProcessingContext;
import org.apache.sling.rewriter.Processor;
import org.apache.sling.rewriter.ProcessorConfiguration;
import org.apache.sling.rewriter.ProcessorManager;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This manager keeps track of configured processors.
*
*/
@Component
@Service(value=ProcessorManager.class)
public class ProcessorManagerImpl
implements ProcessorManager, ResourceChangeListener, ExternalResourceChangeListener {
private static final String CONFIG_REL_PATH = "config/rewriter";
private static final String CONFIG_PATH = "/" + CONFIG_REL_PATH;
protected static final String MIME_TYPE_HTML = "text/html";
/** The logger */
private final Logger log = LoggerFactory.getLogger(this.getClass());
/** The bundle context. */
private BundleContext bundleContext;
@Reference
private ResourceResolverFactory resourceResolverFactory;
/** loaded processor configurations */
private final Map<String, ConfigEntry[]> processors = new HashMap<String, ConfigEntry[]>();
/** Ordered processor configurations. */
private List<ProcessorConfiguration> orderedProcessors = new ArrayList<ProcessorConfiguration>();
/** Event handler registration */
private volatile ServiceRegistration<ResourceChangeListener> eventHandlerRegistration;
/** Search path */
private String[] searchPath;
/** The factory cache. */
private FactoryCache factoryCache;
/**
* Activate this component.
* @param ctx
*/
@Activate
protected void activate(final BundleContext ctx)
throws LoginException, InvalidSyntaxException {
this.bundleContext = ctx;
this.factoryCache = new FactoryCache(this.bundleContext);
// create array of search paths for actions and constraints
this.searchPath = this.initProcessors();
// register event handler
final Dictionary<String, Object> props = new Hashtable<String, Object>();
props.put(ResourceChangeListener.CHANGES,
new String[] { ChangeType.ADDED.toString(), ChangeType.CHANGED.toString(),
ChangeType.REMOVED.toString(), ChangeType.PROVIDER_ADDED.toString(), ChangeType.PROVIDER_REMOVED.toString() });
props.put(ResourceChangeListener.PATHS, "glob:*" + CONFIG_PATH + "/**");
props.put("service.description", "Processor Configuration/Modification Handler");
props.put("service.vendor", "The Apache Software Foundation");
this.eventHandlerRegistration = this.bundleContext.registerService(ResourceChangeListener.class, this,
props);
this.factoryCache.start();
WebConsoleConfigPrinter.register(this.bundleContext, this);
}
@SuppressWarnings("deprecation")
private ResourceResolver createResourceResolver() throws LoginException {
return this.resourceResolverFactory.getAdministrativeResourceResolver(null);
}
/**
* Deactivate this component.
* @param ctx
*/
protected void deactivate(final ComponentContext ctx) {
if ( this.eventHandlerRegistration != null ) {
this.eventHandlerRegistration.unregister();
this.eventHandlerRegistration = null;
}
this.factoryCache.stop();
this.factoryCache = null;
WebConsoleConfigPrinter.unregister();
this.bundleContext = null;
}
@Override
public void onChange(final List<ResourceChange> changes) {
for(final ResourceChange change : changes){
// check if the event handles something in the search paths
String path = change.getPath();
int foundPos = -1;
for(final String sPath : this.searchPath) {
if ( path.startsWith(sPath) ) {
foundPos = sPath.length();
break;
}
}
boolean handled = false;
if ( foundPos != -1 ) {
// now check if this is a rewriter config
// relative path after the search path
final int firstSlash = path.indexOf('/', foundPos);
final int pattern = path.indexOf(CONFIG_PATH, foundPos);
// only if firstSlash and pattern are at the same position, this might be a rewriter config
if ( firstSlash == pattern && firstSlash != -1 ) {
// the node should be a child of CONFIG_PATH
if ( path.length() > pattern + CONFIG_PATH.length() && path.charAt(pattern + CONFIG_PATH.length()) == '/') {
// if a child resource is changed, make sure we have the correct path
final int slashPos = path.indexOf('/', pattern + CONFIG_PATH.length() + 1);
if ( slashPos != -1 ) {
path = path.substring(0, slashPos);
}
// we should do the update async as we don't want to block the event delivery
final String configPath = path;
final Thread t = new Thread() {
@Override
public void run() {
if (change.getType() == ChangeType.REMOVED) {
removeProcessor(configPath);
} else {
updateProcessor(configPath);
}
}
};
t.start();
handled = true;
}
}
}
if ( !handled && change.getType() == ChangeType.REMOVED ) {
final Thread t = new Thread() {
@Override
public void run() {
checkRemoval(change.getPath());
}
};
t.start();
}
}
}
/**
* Initializes the current processors
*/
private synchronized String[] initProcessors() throws LoginException {
try ( final ResourceResolver resolver = this.createResourceResolver()) {
for(final String path : resolver.getSearchPath() ) {
// check if the search path exists
final Resource spResource = resolver.getResource(path.substring(0, path.length() - 1));
if ( spResource != null ) {
// now iterate over the child nodes
final Iterator<Resource> spIter = spResource.listChildren();
while ( spIter.hasNext() ) {
// check if the node has a rewriter config
final Resource appResource = spIter.next();
final Resource parentResource = resolver.getResource(appResource.getPath() + CONFIG_PATH);
if ( parentResource != null ) {
// now read configs
final Iterator<Resource> iter = parentResource.listChildren();
while ( iter.hasNext() ) {
final Resource configResource = iter.next();
final String key = configResource.getName();
final ProcessorConfigurationImpl config = this.getProcessorConfiguration(configResource);
this.log.debug("Found new processor configuration {}", config);
this.addProcessor(key, configResource.getPath(), config);
}
}
}
}
}
return resolver.getSearchPath();
}
}
/**
* Read the configuration for the processor from the repository.
*/
private ProcessorConfigurationImpl getProcessorConfiguration(final Resource configResource) {
final ProcessorConfigurationImpl config = new ProcessorConfigurationImpl(configResource);
return config;
}
/**
* adds a processor configuration
*/
protected void addProcessor(final String key, final String configPath, final ProcessorConfigurationImpl config) {
ConfigEntry[] configs = this.processors.get(key);
if ( configs == null ) {
configs = new ConfigEntry[1];
configs[0] = new ConfigEntry(configPath, config);
} else {
ConfigEntry[] newConfigs = new ConfigEntry[configs.length + 1];
System.arraycopy(configs, 0, newConfigs, 0, configs.length);
newConfigs[configs.length] = new ConfigEntry(configPath, config);
}
this.processors.put(key, configs);
// only add active configurations
if ( config.isActive() ) {
this.orderedProcessors.add(config);
Collections.sort(this.orderedProcessors, new ProcessorConfiguratorComparator());
}
}
private void printConfiguration(final PrintWriter pw, final ConfigEntry entry) {
if ( entry.config instanceof ProcessorConfigurationImpl ) {
((ProcessorConfigurationImpl)entry.config).printConfiguration(pw);
} else {
pw.println(entry.config.toString());
}
pw.print("Resource path: ");
pw.println(entry.path);
}
synchronized void printConfiguration(final PrintWriter pw) {
pw.println("Current Apache Sling Rewriter Configuration");
pw.println("=================================================================");
pw.println("Active Configurations");
pw.println("-----------------------------------------------------------------");
// we process the configs in their order
for(final ProcessorConfiguration config : this.orderedProcessors) {
// search the corresponding full config
for(final Map.Entry<String, ConfigEntry[]> entry : this.processors.entrySet()) {
if ( entry.getValue().length > 0 && entry.getValue()[0].config == config ) {
pw.print("Configuration ");
pw.println(entry.getKey());
pw.println();
printConfiguration(pw, entry.getValue()[0]);
if ( entry.getValue().length > 1 ) {
pw.println("Overriding configurations from the following resource paths: ");
for(int i=1; i < entry.getValue().length; i++) {
pw.print("- ");
pw.println(entry.getValue()[i].path);
}
}
pw.println();
pw.println();
break;
}
}
}
}
/**
* updates a processor
*/
private synchronized void updateProcessor(final String path) {
final int pos = path.lastIndexOf('/');
final String key = path.substring(pos + 1);
int keyIndex = 0;
// search the search path
for(final String sp : this.searchPath) {
if ( path.startsWith(sp) ) {
break;
}
keyIndex++;
}
try ( final ResourceResolver resolver = this.createResourceResolver()) {
final Resource configResource = resolver.getResource(path);
if ( configResource == null ) {
return;
}
final ProcessorConfigurationImpl config = this.getProcessorConfiguration(configResource);
final ConfigEntry[] configs = this.processors.get(key);
if ( configs != null ) {
// search path
int index = -1;
for(int i=0; i<configs.length; i++) {
if ( configs[i].path.equals(path) ) {
index = i;
break;
}
}
if ( index != -1 ) {
// we are already in the array
if ( index == 0 ) {
// we are the first, so remove the old, and add the new
this.orderedProcessors.remove(configs[index].config);
configs[index] = new ConfigEntry(path, config);
if ( config.isActive() ) {
this.orderedProcessors.add(config);
Collections.sort(this.orderedProcessors, new ProcessorConfiguratorComparator());
}
} else {
// we are not the first, so we can simply exchange
configs[index] = new ConfigEntry(path, config);
}
} else {
// now we have to insert the new config at the correct place
int insertIndex = 0;
boolean found = false;
while ( !found && insertIndex < configs.length) {
final ConfigEntry current = configs[insertIndex];
int currentIndex = -1;
for(int i=0; i<searchPath.length; i++) {
if ( current.path.startsWith(searchPath[i]) ) {
currentIndex = i;
break;
}
}
if ( currentIndex >= keyIndex ) {
found = true;
insertIndex = currentIndex;
}
}
if ( !found ) {
// just append
this.addProcessor(key, path, config);
} else {
ConfigEntry[] newArray = new ConfigEntry[configs.length + 1];
int i = 0;
for(final ConfigEntry current : configs) {
if ( i == insertIndex ) {
newArray[i] = new ConfigEntry(path, config);
i++;
}
newArray[i] = current;
i++;
}
this.processors.put(key, newArray);
if ( insertIndex == 0 ) {
// we are the first, so remove the old, and add the new
this.orderedProcessors.remove(configs[1].config);
if ( config.isActive() ) {
this.orderedProcessors.add(config);
Collections.sort(this.orderedProcessors, new ProcessorConfiguratorComparator());
}
}
}
}
} else {
// completely new, just add it
this.addProcessor(key, path, config);
}
} catch ( final LoginException le) {
log.error("Unable to create resource resolver.", le);
}
}
/**
* removes a pipeline
*/
private synchronized void removeProcessor(final String path) {
final int pos = path.lastIndexOf('/');
final String key = path.substring(pos + 1);
// we have to search the config
final ConfigEntry[] configs = this.processors.get(key);
if ( configs != null ) {
// search path
ConfigEntry found = null;
for(final ConfigEntry current : configs) {
if ( current.path.equals(path) ) {
found = current;
break;
}
}
if ( found != null ) {
this.orderedProcessors.remove(found.config);
if ( configs.length == 1 ) {
this.processors.remove(key);
} else {
if ( found == configs[0] ) {
this.orderedProcessors.add(configs[1].config);
Collections.sort(this.orderedProcessors, new ProcessorConfiguratorComparator());
}
ConfigEntry[] newArray = new ConfigEntry[configs.length - 1];
int index = 0;
for(final ConfigEntry current : configs) {
if ( current != found ) {
newArray[index] = current;
index++;
}
}
this.processors.put(key, newArray);
}
}
}
}
private synchronized void checkRemoval(final String path) {
// TODO Auto-generated method stub
}
/**
* @see org.apache.sling.rewriter.ProcessorManager#getProcessor(org.apache.sling.rewriter.ProcessorConfiguration, org.apache.sling.rewriter.ProcessingContext)
*/
@Override
public Processor getProcessor(ProcessorConfiguration configuration,
ProcessingContext context) {
if ( configuration == null ) {
throw new IllegalArgumentException("Processor configuration is missing.");
}
if ( context == null ) {
throw new IllegalArgumentException("Processor context is missing.");
}
boolean isPipeline = false;
if ( configuration instanceof ProcessorConfigurationImpl ) {
isPipeline = ((ProcessorConfigurationImpl)configuration).isPipeline();
} else {
isPipeline = configuration instanceof PipelineConfiguration;
}
try {
if ( isPipeline ) {
final PipelineImpl pipeline = new PipelineImpl(this.factoryCache);
pipeline.init(context, configuration);
return pipeline;
}
final Processor processor = new ProcessorWrapper(configuration, this.factoryCache);
processor.init(context, configuration);
return processor;
} catch (final IOException ioe) {
throw new SlingException("Unable to setup processor: " + ioe.getMessage(), ioe);
}
}
/**
* @see org.apache.sling.rewriter.ProcessorManager#getProcessorConfigurations()
*/
@Override
public List<ProcessorConfiguration> getProcessorConfigurations() {
return this.orderedProcessors;
}
protected static final class ProcessorConfiguratorComparator implements Comparator<ProcessorConfiguration> {
/**
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
@Override
public int compare(ProcessorConfiguration config0, ProcessorConfiguration config1) {
final int o0 = ((ProcessorConfigurationImpl)config0).getOrder();
final int o1 = ((ProcessorConfigurationImpl)config1).getOrder();
if ( o0 == o1 ) {
return 0;
} else if ( o0 < o1 ) {
return 1;
}
return -1;
}
}
public static final class ConfigEntry {
public final String path;
public final ProcessorConfiguration config;
public ConfigEntry(final String p, final ProcessorConfiguration pc) {
this.path = p;
this.config = pc;
}
}
}