org.eclipse.jkube.kit.common.util.PluginServiceFactory Maven / Gradle / Ivy
/**
* Copyright (c) 2019 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at:
*
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.jkube.kit.common.util;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* A simple factory for creating services with no-arg constructors from a textual
* descriptor. This descriptor, which must be a resource loadable by this class'
* classloader, is a plain text file which looks like
*
*
* com.example.MyProjectLabelEnricher
* !org.eclipse.jkube.maven.jkube.enhancer.DefaultProjectLabelEnricher
* com.example.AnotherEnricher,50
*
*
* If a line starts with !
it is removed if it has been added previously.
* The optional second numeric value is the order in which the services are returned.
*
* @author roland
*/
public final class PluginServiceFactory {
private static final int DEFAULT_ORDER = 100;
// Matches comment lines and empty lines. these are skipped
private static final Pattern COMMENT_LINE_PATTERN = Pattern.compile("^(\\s*#.*|\\s*)$");
private final List additionalClassLoaders;
// Parameters for service constructors
private final C context;
public PluginServiceFactory(C context, ClassLoader ... loaders) {
this.context = context;
this.additionalClassLoaders = new ArrayList<>();
Stream.of(loaders).forEach(this::addAdditionalClassLoader);
}
/**
* Create a list of services ordered according to the ordering given in the
* service descriptor files. Note, that the descriptor will be looked up
* in the whole classpath space, which can result in reading in multiple
* descriptors with a single path. Note, that the reading order for multiple
* resources with the same name is not defined.
*
* @param descriptorPaths a list of resource paths which are handle in the given order.
* Normally, default service should be given as first parameter so that custom
* descriptors have a chance to remove a default service.
* @param type of the service objects to create
* @return an ordered list of created services or an empty list.
*/
public List createServiceObjects(String... descriptorPaths) {
try {
ServiceEntry.initDefaultOrder();
TreeMap serviceMap = new TreeMap<>();
for (String descriptor : descriptorPaths) {
readServiceDefinitions(serviceMap, descriptor);
}
return new ArrayList<>(serviceMap.values());
} finally {
ServiceEntry.removeDefaultOrder();
}
}
private void readServiceDefinitions(Map extractorMap, String defPath) {
try {
for (String url : ClassUtil.getResources(defPath, additionalClassLoaders)) {
readServiceDefinitionFromUrl(extractorMap, url);
}
} catch (IOException e) {
throw new IllegalStateException("Cannot load service from " + defPath + ": " + e, e);
}
}
private void readServiceDefinitionFromUrl(Map extractorMap, String url) {
String line = null;
try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(new URL(url).openStream(), StandardCharsets.UTF_8))) {
line = reader.readLine();
while (line != null) {
createOrRemoveService(extractorMap, line);
line = reader.readLine();
}
} catch (ReflectiveOperationException|IOException e) {
throw new IllegalStateException("Cannot load service " + line + " defined in " +
url + " : " + e + ". Aborting", e);
}
}
private synchronized void createOrRemoveService(Map serviceMap, String line)
throws ReflectiveOperationException {
if (line.length() > 0 && !COMMENT_LINE_PATTERN.matcher(line).matches()) {
ServiceEntry entry = new ServiceEntry(line);
if (entry.isRemove()) {
// Removing is a bit complex since we need to find out
// the proper key since the order is part of equals/hash,
// so we can't fetch/remove it directly
Set toRemove = new HashSet<>();
for (ServiceEntry key : serviceMap.keySet()) {
if (key.getClassName().equals(entry.getClassName())) {
toRemove.add(key);
}
}
for (ServiceEntry key : toRemove) {
serviceMap.remove(key);
}
} else {
Class clazz = ClassUtil.classForName(entry.getClassName(), additionalClassLoaders);
if (clazz == null) {
throw new ClassNotFoundException("Class " + entry.getClassName() + " could not be found");
}
T service = clazz.getConstructor(context.getClass()).newInstance(context);
serviceMap.put(entry, service);
}
}
}
public void addAdditionalClassLoader(ClassLoader classLoader) {
this.additionalClassLoaders.add(classLoader);
}
static class ServiceEntry implements Comparable {
private final String className;
private final boolean remove;
private Integer order;
/**
* Initialise with start value for entries without an explicit order.
*/
private static final ThreadLocal DEFAULT_ORDER_HOLDER = ThreadLocal.withInitial(() -> DEFAULT_ORDER);
/**
* Parse an entry in the service definition. This should be the full qualified classname
* of a service, optional prefixed with "!
" in which case the service is removed
* from the default list. An order value can be appended after the classname with a comma for give an
* indication for the ordering of services. If not given, 100 is taken for the first entry, counting up.
*
* @param line line to parse
*/
public ServiceEntry(String line) {
String[] parts = line.split(",");
if (parts[0].startsWith("!")) {
remove = true;
className = parts[0].substring(1);
} else {
remove = false;
className = parts[0];
}
if (parts.length > 1) {
try {
order = Integer.parseInt(parts[1]);
} catch (NumberFormatException exp) {
order = nextDefaultOrder();
}
} else {
order = nextDefaultOrder();
}
}
private Integer nextDefaultOrder() {
Integer defaultOrder = DEFAULT_ORDER_HOLDER.get();
DEFAULT_ORDER_HOLDER.set(defaultOrder + 1);
return defaultOrder;
}
private static void initDefaultOrder() {
DEFAULT_ORDER_HOLDER.set(DEFAULT_ORDER);
}
private static void removeDefaultOrder() {
DEFAULT_ORDER_HOLDER.remove();
}
private String getClassName() {
return className;
}
private boolean isRemove() {
return remove;
}
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (o == null || getClass() != o.getClass()) { return false; }
ServiceEntry that = (ServiceEntry) o;
return className.equals(that.className);
}
@Override
public int hashCode() {
return className.hashCode();
}
/** {@inheritDoc} */
public int compareTo(ServiceEntry o) {
int ret = this.order - o.order;
return ret != 0 ? ret : this.className.compareTo(o.className);
}
}
}