diff --git a/README.md b/README.md
index 4ff2836..6e41664 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,13 @@
# Why you should use this project?
If you want to use a service provider interface (_SPI_) you need to register your service implementation in the _/META-INF/services/<Full qualified spi interface name>_ file.
-Additionally you usually need to write a service locator to be able to use the service implementation.
+Additionally, you usually need to write a service locator to be able to use the service implementation.
The annotation processor offered by this project provides exactly this. It allows you to create the service locator file just by adding an annotation to you spi implementation.
-Additionally it will generate a service locator for you.
+Additionally, it will generate a service locator for you.
+
+# Prerequisites
+- The generated ServiceLocator code requires at least JDK 8.
# Features
Annotation processor that
@@ -76,12 +79,13 @@ To create multiple service locators in the same package use @SpiServiceLocators:
@SpiServiceLocators({
@SpiServiceLocator(FirstExampleSpiInterface.class),
- @SpiServiceLocator(SecondExampleSpiInterface.class),
+ @SpiServiceLocator(SecondExampleSpiInterface.class)
})
package your.target.package;
import io.toolisticon.spiap.api.SpiServiceLocator;
- import your.spi.package.ExampleSpiInterface;
+ import your.spi.package.FirstExampleSpiInterface;
+ import your.spi.package.SecondExampleSpiInterface;
## How to register a service implementation
Just add a Service annotation to your service implementation:
@@ -97,7 +101,7 @@ Just add a Service annotation to your service implementation:
Service annotations mandatory value must declare the SPI you want the service class to be registered to.
All other annotation attributes are optional.
-- id defines a custom id which can be used to locate a specific servics implementation via the generated service locator class. Defaults to fully qualified service class name in generated service locator.
+- id defines a custom id which can be used to locate a specific services implementation via the generated service locator class. Defaults to fully qualified service class name in generated service locator.
- description declares a short description about the implementation
- priority is used to define a specific order in which the services are located
diff --git a/pom.xml b/pom.xml
index 78f805c..22f7bf7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,11 +77,9 @@
UTF-8
1.8
-
${java.version}
${java.version}
-
0.12.0
0.20.0
diff --git a/processor/src/main/resources/ServiceLocator.tpl b/processor/src/main/resources/ServiceLocator.tpl
index e8c4e03..ab65688 100644
--- a/processor/src/main/resources/ServiceLocator.tpl
+++ b/processor/src/main/resources/ServiceLocator.tpl
@@ -1,333 +1,467 @@
package ${ package };
import ${ canonicalName };
+
+import io.toolisticon.spiap.api.OutOfService;
import io.toolisticon.spiap.api.Service;
import io.toolisticon.spiap.api.Services;
-import io.toolisticon.spiap.api.OutOfService;
+import java.io.IOException;
import java.io.InputStream;
-import java.lang.ClassLoader;
-import java.lang.NoClassDefFoundError;
-import java.lang.NumberFormatException;
-import java.lang.Thread;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import java.util.Set;
-import java.util.HashSet;
+import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
/**
- * A generic service locator.
+ * Service locator for ${ simpleName } to provide SPI implementations.
*/
-public class ${ simpleName }ServiceLocator {
-
- private static ClassLoader classLoaderToUse = null;
+public enum ${ simpleName }ServiceLocator {
- /**
- * Caches all loaded services.
- */
- private static List serviceImplementations;
+ INSTANCE;
- /**
- * Exception that is thrown if a specific service implementation can't be found.
- */
- public static class ImplementationNotFoundException extends RuntimeException{
-
- public ImplementationNotFoundException(String id) {
- super(String.format("Couldn't find implementation for spi ${simpleName} with id : '%s'",id));
- }
+ /**
+ * Caches all loaded services until reload is invoked or classloader has been changed.
+ */
+ private final List serviceImplementations = new ArrayList<>();
- }
+ /** Classloader used for service loading. */
+ private ClassLoader classLoaderToUse = null;
- /**
- * Key class for looking checking available spi service implementations.
- */
- public static class ServiceImplementation {
-
- private final String id;
- private final String description;
- private final int priority;
- private final boolean outOfService;
- private final ${ simpleName } serviceInstance;
-
- /**
- * Initial default config
- */
- private ServiceImplementation(${ simpleName } serviceInstance) {
- this.serviceInstance = serviceInstance;
- this.id = serviceInstance.getClass().getCanonicalName();
- this.description = "";
- this.priority = 0;
- this.outOfService = false;
- }
- private ServiceImplementation(ServiceImplementation previousConfig, String id, String description, Integer priority, Boolean outOfService) {
- this.serviceInstance = previousConfig.getServiceInstance();
- this.id = id != null ? id : previousConfig.getId();
- this.description = description != null ? description : previousConfig.getDescription();
- this.priority = priority !=null ? priority : previousConfig.getPriority();
- this.outOfService = outOfService != null ? outOfService : previousConfig.isOutOfService();
- }
+ /**
+ * Get {@link ServiceImplementation} for {@link ${ simpleName }} implementation.
+ *
+ * @param serviceImpl
+ * of type ${ simpleName }
+ * @return ServiceImplementation
+ */
+ public static ServiceImplementation getServiceImplementation(
+ final ${ simpleName } serviceImpl) {
+ // create default config
+ ServiceImplementation serviceKey = new ServiceImplementation(serviceImpl);
- public String getId() {
- return id;
- }
+ // try to get ServiceKey from annotation
+ serviceKey = INSTANCE.getServiceImplementationByAnnotations(serviceKey, serviceImpl);
+ serviceKey = INSTANCE.getServiceImplementationByProperty(serviceKey, serviceImpl);
+ return serviceKey;
- public String getDescription() {
- return description;
- }
+ }
- public int getPriority() {
- return priority;
- }
- public boolean isOutOfService () {
- return outOfService;
- }
+ /**
+ * Get {@link ServiceImplementation} for all available implementations.
+ *
+ * @return a list that contains ServiceImplementations for all available service
+ * implementations, or an empty list if none could be found.
+ */
+ public static List getServiceImplementations() {
- public ${ simpleName } getServiceInstance () {
- return serviceInstance;
- }
+ return new ArrayList<>(INSTANCE.loadImplementations());
+ }
- }
+ /**
+ * Locate a service implementation with a specific id.
+ *
+ * @param id
+ * the id to search
+ * @return the service implementation with the id
+ * @throws ImplementationNotFoundException
+ * if either passed id is null or if the service implementation can't be found.
+ */
+ public static ${ simpleName } locateById(final String id) {
+ return locateImplById(id).orElseThrow(() -> new ImplementationNotFoundException(id));
+ }
- private static ServiceImplementation getServiceImplementationByAnnotations(ServiceImplementation previousConfig, ${ simpleName } serviceImpl) {
-
- try {
- Service serviceAnnotation = serviceImpl.getClass().getAnnotation(Service.class);
- if (serviceAnnotation == null) {
- Services servicesAnnotation = serviceImpl.getClass().getAnnotation(Services.class);
- if (servicesAnnotation != null) {
- for (Service service : servicesAnnotation.value()) {
- if (${ simpleName }.class.equals(service.value())) {
- serviceAnnotation = service;
- break;
- }
- }
- }
- }
- String id = serviceAnnotation != null ? serviceAnnotation.id() : null;
- String description = serviceAnnotation != null ? serviceAnnotation.description() : null;
- Integer priority = serviceAnnotation != null ? serviceAnnotation.priority() : null;
- boolean outOfService = serviceImpl.getClass().getAnnotation(OutOfService.class) != null;
-
- return new ServiceImplementation(previousConfig, id, description, priority, outOfService);
-
- } catch (NoClassDefFoundError e) {
- // ignore
- }
- return previousConfig;
+ /**
+ * Locate a service implementation with a specific id.
+ *
+ * @param id
+ * the id to search for.
+ * @return {@code Optional<${ simpleName }>} service implementation identified by the
+ * given id.
+ */
+ public static Optional<${ simpleName }> locateImplById(final String id) {
+ if (id == null) {
+ return Optional.empty();
}
- private static ServiceImplementation getServiceImplementationByProperty(ServiceImplementation previousConfig, ${ simpleName } serviceImpl) {
-
- // first try to get config from property file
- final String propertyFileName = "/META-INF/spiap/" + ${ simpleName }.class.getCanonicalName() + "/" + serviceImpl.getClass().getCanonicalName() + ".properties";
- InputStream inputStream = ${ simpleName }ServiceLocator.class.getResourceAsStream(propertyFileName);
- if (inputStream != null) {
-
- try {
- Properties properties = new Properties();
- properties.load(inputStream);
-
- String id = properties.getProperty("${constants.id}");
- String description = properties.getProperty("${constants.description}");
- Integer priority = null;
- try {
- String priorityString = properties.getProperty("${constants.priority}");
- if (priorityString != null) {
- priority = Integer.valueOf(priorityString);
- }
- } catch (NumberFormatException e) {
- // ignore
- }
-
- Boolean outOfService = null;
- String outOfServiceString = properties.getProperty("${constants.outOfService}");
- if (outOfServiceString != null) {
- outOfService = Boolean.valueOf(outOfServiceString);
- }
-
- return new ServiceImplementation(previousConfig, id, description, priority, outOfService);
-
- } catch (Exception e) {
- // do nothing
- }
- }
-
- return previousConfig;
-
+ return getServiceImplementations().stream()
+ .filter(si -> id.equals(si.getId()))
+ .findFirst()
+ .map(ServiceImplementation::getServiceInstance);
+ }
+
+
+ /**
+ * Locate a service implementation.
+ *
+ * If more than one implementation is available, the first one is returned.
+ *
+ * Important: Successive calls may return different service implementations.
+ *
+ * @return First implementation found via locateAll method call.
+ */
+ public static ${ simpleName } locate() {
+
+ final List<${ simpleName }> services = locateAll();
+ return services.isEmpty()
+ ? null
+ : services.get(0);
+ }
+
+
+ /**
+ * Locate all available service implementations.
+ *
+ * @return List containing all available service implementations or an empty list, if no
+ * implementation can be found.
+ */
+ public static List<${ simpleName }> locateAll() {
+
+ return getServiceImplementations().stream()
+ .map(ServiceImplementation::getServiceInstance)
+ .collect(Collectors.toList());
+ }
+
+
+ /**
+ * Clear cache and reload services.
+ */
+ public static void reloadServices() {
+
+ INSTANCE.serviceImplementations.clear();
+ INSTANCE.loadImplementations();
+ }
+
+
+ /**
+ * Load service implementations if cache is empty.
+ *
+ * @return {@code List} containing the currently available service
+ * implementations for the classloader.
+ */
+ private List loadImplementations() {
+
+ if (!serviceImplementations.isEmpty()) {
+ return serviceImplementations;
}
+ final Iterator<${ simpleName }> iterator =
+ ServiceLoader.load(${ simpleName }.class, getClassLoaderToUse()).iterator();
+
+ serviceImplementations.addAll(
+ StreamSupport.stream(Spliterators.spliteratorUnknownSize(
+ iterator, Spliterator.ORDERED | Spliterator.NONNULL), false)
+ .map(${ simpleName }ServiceLocator::getServiceImplementation)
+ .filter(si -> !si.isOutOfService())
+ .sorted()
+ .collect(Collectors.toList()));
+ return serviceImplementations;
+ }
+
+
+ /**
+ * Get the service implementation by annotation.
+ *
+ * @param previousConfig
+ * of type ServiceImplementation
+ * @param serviceImpl
+ * of type ${ simpleName }
+ * @return ServiceImplementation
+ */
+ private ServiceImplementation getServiceImplementationByAnnotations(
+ final ServiceImplementation previousConfig,
+ final ${ simpleName } serviceImpl) {
+
+ try {
+ final boolean outOfService = serviceImpl.getClass().getAnnotation(OutOfService.class) != null;
+
+ return getServiceAnnotation(serviceImpl)
+ .map(s -> new ServiceImplementation(
+ previousConfig, s.id(), s.description(), s.priority(), outOfService))
+ .orElse(new ServiceImplementation(
+ previousConfig, null, null, null, outOfService));
+ } catch (final NoClassDefFoundError ignore) {
+ // ignore
+ }
- public static ServiceImplementation getServiceImplementation(${ simpleName } serviceImpl) {
+ return previousConfig;
+ }
- // create default config
- ServiceImplementation serviceKey = new ServiceImplementation(serviceImpl);
- // try to get ServiceKey from annotation
- serviceKey = getServiceImplementationByAnnotations(serviceKey, serviceImpl);
+ /**
+ * Find the service by annotation.
+ *
+ * @param serviceImpl
+ * of type ${ simpleName }
+ * @return {@code Optional}
+ */
+ private Optional getServiceAnnotation(
+ final ${ simpleName } serviceImpl) {
- serviceKey = getServiceImplementationByProperty(serviceKey, serviceImpl);
+ final Service serviceAnnotation = serviceImpl.getClass().getAnnotation(Service.class);
+ if (serviceAnnotation != null) {
+ return Optional.of(serviceAnnotation);
+ }
- return serviceKey;
+ final Services servicesAnnotation = serviceImpl.getClass().getAnnotation(Services.class);
+ if (servicesAnnotation != null) {
+ return Arrays.stream(servicesAnnotation.value())
+ .filter(service -> ${ simpleName }.class.equals(service.value()))
+ .findFirst();
+ }
+ return Optional.empty();
+ }
+
+
+ /**
+ * Read the spiap properties file and get ServiceImplementation based on the properties.
+ *
+ * If not found, return previousConfig implementation.
+ *
+ * @param previousConfig
+ * of type ServiceImplementation
+ * @param serviceImpl
+ * of type ${ simpleName }
+ * @return ServiceImplementation
+ */
+ private ServiceImplementation getServiceImplementationByProperty(
+ final ServiceImplementation previousConfig,
+ final ${ simpleName } serviceImpl) {
+
+ // first try to get config from property file
+ final String propertyFileName = String.format("/META-INF/spiap/%s/%s.properties",
+ ${ simpleName }.class.getCanonicalName(),
+ serviceImpl.getClass().getCanonicalName());
+
+ return loadServiceProperties(propertyFileName)
+ .flatMap(p -> getServiceImplementationFromProperties(previousConfig, p))
+ .orElse(previousConfig);
+ }
+
+
+ /**
+ * Build ServiceImplementation based on the given properties. If an error occurs return
+ * Optional.empty.
+ *
+ * @param previousConfig
+ * of type ServiceImplementation
+ * @param properties
+ * of type Properties
+ * @return {@code Optional}
+ */
+ private Optional getServiceImplementationFromProperties(
+ final ServiceImplementation previousConfig,
+ final Properties properties) {
+
+ if (properties == null) {
+ return Optional.empty();
+ }
+ try {
+ final String id = properties.getProperty("id");
+ final String description = properties.getProperty("description");
+ Integer priority = null;
+ try {
+ final String priorityString = properties.getProperty("priority");
+ if (priorityString != null) {
+ priority = Integer.valueOf(priorityString);
+ }
+ } catch (final NumberFormatException e) {
+ // ignore
+ }
+
+ Boolean outOfService = null;
+ final String outOfServiceString = properties.getProperty("outOfService");
+ if (outOfServiceString != null) {
+ outOfService = Boolean.valueOf(outOfServiceString);
+ }
+ return Optional.of(
+ new ServiceImplementation(previousConfig, id, description, priority, outOfService));
+ } catch (final Exception ignore) {
+ // do nothing
+ }
+ return Optional.empty();
+ }
+
+
+ /**
+ * Load the spiap properties file from resources. In terms, an error occurs return an empty
+ * Optional.
+ *
+ * @param propertyFileName
+ * of type String
+ * @return {@code Optional}
+ */
+ private Optional loadServiceProperties(final String propertyFileName) {
+
+ try (final InputStream inputStream =
+ ${ simpleName }ServiceLocator.class.getResourceAsStream(propertyFileName)) {
+ if (inputStream == null) {
+ return Optional.empty();
+ }
+ final Properties properties = new Properties();
+ properties.load(inputStream);
+ return Optional.of(properties);
+ } catch (final IOException ignore) {
+ // No properties found. Ignore.
}
+ return Optional.empty();
+ }
- /**
- * Comparator which allows sorting of service implementations by priority.
- */
- public static class ServicePriorityComparator implements Comparator {
+ /**
+ * Method getClassLoaderToUse returns the classLoaderToUse of this
+ * ${ simpleName }ServiceLocator object.
+ *
+ *
+ *
+ * @return the classLoaderToUse (type ClassLoader) of this ${ simpleName }ServiceLocator
+ * object.
+ */
+ private ClassLoader getClassLoaderToUse() {
- public int compare (ServiceImplementation o1,ServiceImplementation o2){
- if (o1 == null && o2 == null) {
- return 0;
- } else if (o1 != null && o2 == null) {
- return -1;
- } else if (o1 == null && o2 != null) {
- return -1;
- } else {
+ return classLoaderToUse != null
+ ? classLoaderToUse
+ : Thread.currentThread().getContextClassLoader();
+ }
- if (o1.priority == o2.priority) {
- return 0;
- } else {
- return o1.getPriority() < o2.getPriority() ? -1 : 1;
- }
- }
- }
+ /**
+ * Set the classloader to use.
+ * @param classLoader
+ * The class loader to be used to load provider-configuration files and provider classes,
+ * or null if the currents thread is to be used
+ */
+ public static void setClassLoaderToUse(final ClassLoader classLoader) {
- }
+ INSTANCE.serviceImplementations.clear();
+ INSTANCE.classLoaderToUse = classLoader;
+ }
- /*
- * Get {@link ServiceKey} for all available implementations.
- * @return a list that contains ServiceKeys for all available service implementations, or an empty List if none could be found.
- */
- public static List<${ simpleName }ServiceLocator.ServiceImplementation> getServiceImplementations() {
-
- locateAll();
- return new ArrayList(serviceImplementations);
- }
+ /**
+ * Exception that is thrown if a specific service implementation cannot be found.
+ */
+ public static class ImplementationNotFoundException extends RuntimeException {
- /**
- * locate a service implementation with a specific id.
- *
- * @param id the id to search
- * @return the service implementation with the id
- * @throws ImplementationNotFoundException if either passed id is null or if the service implementation can't be found.
- */
- public static ${ simpleName } locateById(String id) {
+ private static final long serialVersionUID = -8610400812558665841L;
- if (id != null) {
- locateAll();
+ public ImplementationNotFoundException(final String id) {
- for (ServiceImplementation serviceImpl : serviceImplementations) {
+ super(String.format(
+ "Could not find implementation for spi ${ simpleName } with id : '%s'", id));
+ }
+ }
- if (id.equals(serviceImpl.getId())) {
- return serviceImpl.getServiceInstance();
- }
- }
- }
+ /**
+ * Key class for looking for available spi service implementations.
+ */
+ public static class ServiceImplementation implements Comparable {
- throw new ImplementationNotFoundException(id);
+ private final String id;
+ private final String description;
+ private final int priority;
+ private final boolean outOfService;
+ private final ${ simpleName } serviceInstance;
- }
/**
- * Hide constructor.
+ * Initial default config
*/
- private ${ simpleName }ServiceLocator() {
- }
+ private ServiceImplementation(final ${ simpleName } serviceInstance) {
- /**
- * Locates a service implementation.
- * The first implementation returned by the locateAll method will be returned if more than one implementation is available.
- * Successive calls may return different service implementations.
- * @return returns the first Implementation found via locateAll method call.
- */
- public static ${ simpleName } locate() {
- final List services = locateAll();
- return services.isEmpty() ? (${ simpleName })null : (${ simpleName })services.get(0);
+ this.serviceInstance = serviceInstance;
+ this.id = serviceInstance.getClass().getCanonicalName();
+ this.description = "";
+ this.priority = 0;
+ this.outOfService = false;
}
- /**
- * Locates all available service implementations.
- * @return returns a list containing all available service implementations or an empty list, if no implementation can be found.
- */
- public static List< ${ simpleName } > locateAll() {
- if (serviceImplementations == null) {
+ private ServiceImplementation(
+ final ServiceImplementation previousConfig,
+ final String id,
+ final String description,
+ final Integer priority,
+ final Boolean outOfService) {
- final Iterator<${ simpleName }> iterator = ServiceLoader.load(${ simpleName }.class, getClassLoaderToUse()).iterator();
- final List tmpServiceImplementations = new ArrayList();
+ this.serviceInstance = previousConfig.getServiceInstance();
+ this.id = id != null ? id : previousConfig.getId();
+ this.description = description != null ? description : previousConfig.getDescription();
+ this.priority = priority != null ? priority : previousConfig.getPriority();
+ this.outOfService = outOfService != null ? outOfService : previousConfig.isOutOfService();
+ }
- while (iterator.hasNext()) {
- try {
- ${ simpleName } service = iterator.next();
+ public String getId() {
- ServiceImplementation serviceImpl = getServiceImplementation(service);
- if (!serviceImpl.isOutOfService()) {
- tmpServiceImplementations.add(serviceImpl);
- }
+ return id;
+ }
- }
- catch (Error e) {
- e.printStackTrace(System.err);
- }
- }
- Collections.sort(tmpServiceImplementations, new ServicePriorityComparator());
+ public String getDescription() {
- serviceImplementations = tmpServiceImplementations;
+ return description;
+ }
- }
- // Now return services
- final List<${ simpleName }> services = new ArrayList<${ simpleName }>();
+ public int getPriority() {
- for (ServiceImplementation service : serviceImplementations) {
- services.add(service.getServiceInstance());
- }
+ return priority;
+ }
- return services;
+ public boolean isOutOfService() {
+
+ return outOfService;
}
- /**
- * Clear cache and reload services.
- */
- public static void reloadServices() {
- serviceImplementations = null;
- locateAll();
- }
+ public ${ simpleName } getServiceInstance() {
- /**
- * Sets the classloader to use.
- * @param classLoader The class loader to be used to load provider-configuration files and provider classes, or null if the currents thread is to be used
- */
- public static void setClassLoaderToUse(ClassLoader classLoader) {
- classLoaderToUse = classLoader;
+ return serviceInstance;
}
- private static ClassLoader getClassLoaderToUse () {
- return classLoaderToUse != null ? classLoaderToUse : Thread.currentThread().getContextClassLoader();
+ /**
+ * Compare by priority.
+ *
+ * @param o
+ * of type ServiceImplementation
+ *
+ * @return int
+ */
+ @Override
+ public int compareTo(final ServiceImplementation o) {
+
+ if (o == null) {
+ return -1;
+ }
+ if (this == o) {
+ return 0;
+ }
+ if (this.getPriority() == o.getPriority()) {
+ return 0;
+ } else {
+ return this.getPriority() < o.getPriority() ? -1 : 1;
+ }
}
-
+ }
}