/*******************************************************************************
* Copyright (c) 2007, 2015 David Green and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.wikitext.util;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage;
import org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguageProvider;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
/**
* A service locator for use both inside and outside of an Eclipse environment. Provides access to markup languages by
* name.
* <p>
* Markup languages may be dynamically discovered by adding a Java service file in one of the following locations:
* <ul>
* <li><tt>META-INF/services/org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage</tt></li>
* <li><tt>services/org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage</tt></li>
* <li><tt>META-INF/services/org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguageProvider</tt></li>
* <li><tt>services/org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguageProvider</tt></li>
* </ul>
* </p>
*
* @author David Green
* @see MarkupLanguage
* @see MarkupLanguageProvider
* @since 3.0
*/
public class ServiceLocator {
protected final ClassLoader classLoader;
private static Object implementationClassLock = new Object();
private static Class<? extends ServiceLocator> implementationClass;
private static Pattern CLASS_NAME_PATTERN = Pattern.compile("\\s*([^\\s#]+)?#?.*"); //$NON-NLS-1$
protected ServiceLocator(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Get an instance of the service locator
*
* @param classLoader
* the class loader to use when looking up services
* @see #getInstance()
*/
public static ServiceLocator getInstance(ClassLoader classLoader) {
synchronized (implementationClassLock) {
if (implementationClass != null) {
try {
return implementationClass.getConstructor(ClassLoader.class).newInstance(classLoader);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
return new ServiceLocator(classLoader);
}
/**
* Get an instance of the service locator
*
* @see #getInstance(ClassLoader)
*/
public static ServiceLocator getInstance() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
ServiceLocator.class.getClassLoader();
}
return getInstance(loader);
}
/**
* get a markup language by name
*
* @param languageName
* the {@link MarkupLanguage#getName() name} of the markup language, or the fully qualified name of the
* class that implements the language
* @return the language implementation
* @throws IllegalArgumentException
* if the provided language name is null or if no implementation is available for the given language
*/
public MarkupLanguage getMarkupLanguage(final String languageName) throws IllegalArgumentException {
checkArgument(!Strings.isNullOrEmpty(languageName), "Must provide a languageName"); //$NON-NLS-1$
Pattern classNamePattern = Pattern.compile("\\s*([^\\s#]+)?#?.*"); //$NON-NLS-1$
// first try Java services (jar-based)
final List<String> names = new ArrayList<>();
final List<MarkupLanguage> languages = new ArrayList<>();
final MarkupLanguage[] result = new MarkupLanguage[1];
loadMarkupLanguages(new MarkupLanguageVisitor() {
public boolean accept(MarkupLanguage language) {
if (languageName.equals(language.getName())) {
result[0] = language;
return false;
}
languages.add(language);
names.add(language.getName());
return true;
}
});
if (result[0] != null) {
return result[0];
}
// next attempt to load the markup language as if the language name is a fully qualified name
Matcher matcher = classNamePattern.matcher(languageName);
if (matcher.matches()) {
String className = matcher.group(1);
if (className != null) {
// first try to load from a discovered markup language since this will circumvent
// classloader issues
for (MarkupLanguage language : languages) {
if (className.equals(language.getClass().getName())) {
return language;
}
}
try {
Class<?> clazz = Class.forName(className, true, classLoader);
if (MarkupLanguage.class.isAssignableFrom(clazz)) {
MarkupLanguage instance = (MarkupLanguage) clazz.newInstance();
return instance;
}
} catch (Exception e) {
// ignore
}
}
}
Collections.sort(names);
// specified language not found.
// create a useful error message
StringBuilder buf = new StringBuilder();
for (String name : names) {
if (buf.length() != 0) {
buf.append(", "); //$NON-NLS-1$
}
buf.append('\'');
buf.append(name);
buf.append('\'');
}
throw new IllegalArgumentException(MessageFormat.format(Messages.getString("ServiceLocator.4"), //$NON-NLS-1$
languageName, buf.length() == 0
? Messages.getString("ServiceLocator.5") //$NON-NLS-1$
: Messages.getString("ServiceLocator.6") + buf)); //$NON-NLS-1$
}
/**
* Get all known markup languages
*/
public Set<MarkupLanguage> getAllMarkupLanguages() {
final Set<MarkupLanguage> markupLanguages = new HashSet<MarkupLanguage>();
loadMarkupLanguages(new MarkupLanguageVisitor() {
public boolean accept(MarkupLanguage language) {
markupLanguages.add(language);
return true;
}
});
return filterDuplicates(markupLanguages);
}
private Set<MarkupLanguage> filterDuplicates(Set<MarkupLanguage> markupLanguages) {
Multimap<String, Class<?>> markupLanguageClassesByName = HashMultimap.create();
ImmutableSet.Builder<MarkupLanguage> builder = ImmutableSet.builder();
for (MarkupLanguage language : markupLanguages) {
if (markupLanguageClassesByName.put(language.getName(), language.getClass())) {
builder.add(language);
}
}
return builder.build();
}
public static void setImplementation(Class<? extends ServiceLocator> implementationClass) {
synchronized (implementationClassLock) {
ServiceLocator.implementationClass = implementationClass;
}
}
interface MarkupLanguageVisitor {
public boolean accept(MarkupLanguage language);
}
void loadMarkupLanguages(MarkupLanguageVisitor visitor) {
for (ResourceDescriptor descriptor : discoverServiceResources()) {
List<String> classNames = readServiceClassNames(descriptor.getUrl());
for (String className : classNames) {
try {
Class<?> clazz = loadClass(descriptor, className);
if (MarkupLanguage.class.isAssignableFrom(clazz)) {
MarkupLanguage instance = (MarkupLanguage) clazz.newInstance();
if (!visitor.accept(instance)) {
return;
}
} else if (MarkupLanguageProvider.class.isAssignableFrom(clazz)) {
MarkupLanguageProvider provider = (MarkupLanguageProvider) clazz.newInstance();
for (MarkupLanguage language : provider.getMarkupLanguages()) {
if (!visitor.accept(language)) {
return;
}
}
}
} catch (Exception e) {
logFailure(className, e);
}
}
}
}
void logFailure(String className, Exception e) {
// very unusual, but inform the user in a stand-alone way
Logger.getLogger(ServiceLocator.class.getName()).log(Level.WARNING,
MessageFormat.format(Messages.getString("ServiceLocator.0"), className), e); //$NON-NLS-1$
}
/**
*
*/
protected static class ResourceDescriptor {
private final URL url;
public ResourceDescriptor(URL url) {
this.url = checkNotNull(url);
}
public URL getUrl() {
return url;
}
}
/**
* Loads the specified class.
*
* @param resourceUrl
* the service resource from which the class name was discovered
* @param className
* the class name to load
* @return the class
* @throws ClassNotFoundException
* if the class could not be loaded
*/
protected Class<?> loadClass(ResourceDescriptor resource, String className) throws ClassNotFoundException {
return Class.forName(className, true, classLoader);
}
/**
* @return the service resources
* @see #getClasspathServiceResourceNames()
*/
protected List<ResourceDescriptor> discoverServiceResources() {
List<ResourceDescriptor> serviceResources = new ArrayList<>();
for (String serviceResourceName : getClasspathServiceResourceNames()) {
try {
Enumeration<URL> resources = classLoader.getResources(serviceResourceName);
while (resources.hasMoreElements()) {
serviceResources.add(new ResourceDescriptor(resources.nextElement()));
}
} catch (IOException e) {
logReadServiceClassNamesFailure(e);
}
}
return serviceResources;
}
/**
* Provides the list of service resource names from which Java services should be loaded.
*
* @return the list of service resource names
*/
protected List<String> getClasspathServiceResourceNames() {
List<String> paths = new ArrayList<>();
for (String suffix : new String[] { "services/" + MarkupLanguage.class.getName(), //$NON-NLS-1$
"services/" + MarkupLanguageProvider.class.getName() }) { //$NON-NLS-1$
for (String prefix : new String[] { "", "META-INF/" }) { //$NON-NLS-1$//$NON-NLS-2$
paths.add(prefix + suffix);
}
}
return ImmutableList.copyOf(paths);
}
/**
* Reads the services provided in the file with the given URL. The URL must provide a file in the format expected by
* {@link ServiceLoader}.
*
* @see ServiceLoader
*/
protected List<String> readServiceClassNames(URL url) {
try (InputStream stream = url.openStream()) {
return readServiceClassNames(stream);
} catch (IOException e) {
logReadServiceClassNamesFailure(e);
}
return Collections.emptyList();
}
List<String> readServiceClassNames(InputStream stream) {
List<String> serviceClassNames = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, Charsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = CLASS_NAME_PATTERN.matcher(line);
if (matcher.matches()) {
String className = matcher.group(1);
if (className != null) {
serviceClassNames.add(className);
}
}
}
} catch (IOException e) {
logReadServiceClassNamesFailure(e);
}
return serviceClassNames;
}
void logReadServiceClassNamesFailure(IOException e) {
// very unusual, but inform in a stand-alone way
Logger.getLogger(ServiceLocator.class.getName()).log(Level.SEVERE, Messages.getString("ServiceLocator.1"), e); //$NON-NLS-1$
}
}