blob: bfe00b034f47de51d6f6ea5f2280c9ccc5ba3b05 [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.openide.util.lookup;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.Lookup;
import org.openide.util.lookup.implspi.SharedClassObjectBridge;
/**
* @author Jaroslav Tulach, Jesse Glick
* @see Lookups#metaInfServices(ClassLoader,String)
* @see "#14722"
*/
final class MetaInfServicesLookup extends AbstractLookup {
static final Logger LOGGER = Logger.getLogger(MetaInfServicesLookup.class.getName());
private static final MetaInfCache CACHE = new MetaInfCache(512);
private static Reference<Executor> RP = new WeakReference<Executor>(null);
static synchronized Executor getRP() {
Executor res = RP.get();
if (res == null) {
try {
Class<?> seek = Class.forName("org.openide.util.RequestProcessor");
res = (Executor)seek.newInstance();
} catch (Throwable t) {
try {
res = Executors.newSingleThreadExecutor();
} catch (Throwable t2) {
res = new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
};
}
}
RP = new SoftReference<Executor>(res);
}
return res;
}
/** A set of all requested classes.
* Note that classes that we actually succeeded on can never be removed
* from here because we hold a strong reference to the loader.
* However we also hold classes which are definitely not loadable by
* our loader.
*/
private final Map<Class<?>,Object> classes = new WeakHashMap<Class<?>,Object>();
/** class loader to use */
private final ClassLoader loader;
/** prefix to prepend */
private final String prefix;
/** Create a lookup reading from a specified classloader.
*/
@SuppressWarnings("LeakingThisInConstructor")
public MetaInfServicesLookup(ClassLoader loader, String prefix) {
this.loader = loader;
this.prefix = prefix;
LOGGER.log(Level.FINE, "Created: {0}", this);
}
@Override
public String toString() {
return "MetaInfServicesLookup[" + loader + "]"; // NOI18N
}
/** Initialize soon, before result's listeners are activated
*/
@Override
void beforeLookupResult(Template<?> template) {
beforeLookup(template);
}
/* Tries to load appropriate resources from manifest files.
*/
@Override
protected final void beforeLookup(Lookup.Template<?> t) {
Class<?> c = t.getType();
Collection<AbstractLookup.Pair<?>> toAdd = null;
synchronized (this) {
if (classes.get(c) == null) { // NOI18N
toAdd = new ArrayList<Pair<?>>();
} else {
// ok, nothing needs to be done
return;
}
}
if (toAdd != null) {
Set<Class<?>> all = new HashSet<Class<?>>();
for (Class type : allSuper(c, all)) {
search(type, toAdd);
}
}
HashSet<R> listeners = null;
synchronized (this) {
if (classes.put(c, "") == null) { // NOI18N
// Added new class, search for it.
LinkedHashSet<AbstractLookup.Pair<?>> lhs = getPairsAsLHS();
List<Item> arr = new ArrayList<Item>();
for (Pair<?> lh : lhs) {
arr.add((Item)lh);
}
for (Pair<?> p : toAdd) {
insertItem((Item) p, arr);
}
listeners = setPairsAndCollectListeners(arr);
}
}
if (listeners != null) {
notifyIn(getRP(), listeners);
}
}
private Set<Class<?>> allSuper(Class<?> clazz, Set<Class<?>> all) {
all.add(clazz);
Class<?> sup = clazz.getSuperclass();
if (sup != null && sup != Object.class) {
all.add(sup);
}
for (Class<?> c : clazz.getInterfaces()) {
allSuper(c, all);
}
return all;
}
/** Finds all pairs and adds them to the collection.
*
* @param clazz class to find
* @param result collection to add Pair to
*/
private void search(Class<?> clazz, Collection<AbstractLookup.Pair<?>> result) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.log(Level.FINER, "Searching for {0} in {1} from {2}", new Object[] {clazz.getName(), clazz.getClassLoader(), this});
}
String res = prefix + clazz.getName(); // NOI18N
Enumeration<URL> en;
try {
en = loader.getResources(res);
} catch (IOException ioe) {
// do not use ErrorManager because we are in the startup code
// and ErrorManager might not be ready
ioe.printStackTrace();
return;
}
// Do not create multiple instances in case more than one JAR
// has the same entry in it (and they load to the same class).
// Probably would not happen, assuming JARs only list classes
// they own, but just in case...
List<Item> foundClasses = new ArrayList<Item>();
Collection<Class<?>> removeClasses = new ArrayList<Class<?>>();
boolean foundOne = false;
while (en.hasMoreElements()) {
if (!foundOne) {
foundOne = true;
// Double-check that in fact we can load the *interface* class.
// For example, say class I is defined in two JARs, J1 and J2.
// There is also an implementation M1 defined in J1, and another
// implementation M2 defined in J2.
// Classloaders C1 and C2 are made from J1 and J2.
// A MetaInfServicesLookup is made from C1. Then the user asks to
// lookup I as loaded from C2. J1 has the services line and lists
// M1, and we can in fact make it. However it is not of the desired
// type to be looked up. Don't do this check, which could be expensive,
// unless we expect to be getting some results, however.
Class<?> realMcCoy = null;
try {
realMcCoy = loader.loadClass(clazz.getName());
} catch (ClassNotFoundException cnfe) {
// our loader does not know about it, OK
}
if (realMcCoy != clazz) {
// Either the interface class is not available at all in our loader,
// or it is not the same version as we expected. Don't provide results.
if (realMcCoy != null) {
LOGGER.log(Level.WARNING, "{0} is not the real McCoy! Actually found it in {1} (from {2}) but searched for from {3}",
new Object[] {clazz.getName(), realMcCoy.getClassLoader(), loader, clazz.getClassLoader()}); // NOI18N
} else {
LOGGER.log(Level.WARNING, "{0} could not be found in {1}", new Object[] {clazz.getName(), loader}); // NOI18N
}
return;
}
}
URL url = en.nextElement();
Item currentItem = null;
try {
InputStream is = url.openStream();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
// XXX consider using ServiceLoaderLine instead
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
// is it position attribute?
if (line.startsWith("#position=")) {
if (currentItem == null) {
LOGGER.log(Level.INFO, "Found line ''{0}'' in {1} but there is no item to associate it with", new Object[] {line, url});
continue;
}
try {
currentItem.position = Integer.parseInt(line.substring(10));
} catch (NumberFormatException e) {
// do not use ErrorManager because we are in the startup code
// and ErrorManager might not be ready
e.printStackTrace();
}
}
if (currentItem != null) {
insertItem(currentItem, foundClasses);
currentItem = null;
}
// Ignore blank lines and comments.
if (line.length() == 0) {
continue;
}
boolean remove = false;
if (line.charAt(0) == '#') {
if ((line.length() == 1) || (line.charAt(1) != '-')) {
continue;
}
// line starting with #- is a sign to remove that class from lookup
remove = true;
line = line.substring(2);
}
Class<?> inst = null;
try {
Object ldr = url.getContent(new Class[] { ClassLoader.class });
if (ldr instanceof ClassLoader) {
inst = Class.forName(line, false, (ClassLoader)ldr);
}
} catch (LinkageError err) {
// go on
} catch (ClassNotFoundException ex) {
LOGGER.log(Level.FINER, "No class found in " + url, ex);
} catch (IOException ex) {
LOGGER.log(Level.FINER, "URL does not support classloader protocol " + url, ex);
}
if (inst == null) try {
// Most lines are fully-qualified class names.
inst = Class.forName(line, false, loader);
} catch (LinkageError err) {
if (remove) {
continue;
}
throw new ClassNotFoundException(err.getMessage(), err);
} catch (ClassNotFoundException cnfe) {
if (remove) {
// if we are removing somthing and the something
// cannot be found it is ok to do nothing
continue;
} else {
// but if we are not removing just rethrow
throw cnfe;
}
}
if (!clazz.isAssignableFrom(inst)) {
throw new ClassNotFoundException(clazzToString(inst) + " not a subclass of " + clazzToString(clazz)); // NOI18N
}
if (remove) {
removeClasses.add(inst);
} else {
// create new item here, but do not put it into
// foundClasses array yet because following line
// might specify its position
currentItem = new Item(inst);
}
}
if (currentItem != null) {
insertItem(currentItem, foundClasses);
currentItem = null;
}
} finally {
is.close();
}
} catch (ClassNotFoundException ex) {
LOGGER.log(Level.INFO, null, ex);
} catch (IOException ex) {
LOGGER.log(Level.INFO, null, ex);
}
}
LOGGER.log(Level.FINER, "Found impls of {0}: {1} and removed: {2} from: {3}", new Object[] {clazz.getName(), foundClasses, removeClasses, this});
/* XXX makes no sense, wrong types:
foundClasses.removeAll(removeClasses);
*/
for (Item item : foundClasses) {
if (removeClasses.contains(item.clazz())) {
continue;
}
result.add(item);
}
}
private static String clazzToString(Class<?> clazz) {
String loc = null;
try {
if (clazz.getProtectionDomain() != null && clazz.getProtectionDomain().getCodeSource() != null) {
loc = clazz.getProtectionDomain().getCodeSource().getLocation().toString();
}
} catch (Throwable ex) {
loc = ex.getMessage();
}
return clazz.getName() + "@" + clazz.getClassLoader() + ":" + loc; // NOI18N
}
/**
* Insert item to the list according to item.position value.
*/
private void insertItem(Item item, List<Item> list) {
// no position? -> add it to the end
if (item.position == -1) {
if (!list.contains(item)) {
list.add(item);
}
return;
}
int foundIndex = -1;
int index = -1;
for (Item i : list) {
if (i.equals(item)) {
return;
}
index++;
if (foundIndex < 0) {
if (i.position == -1 || i.position > item.position) {
foundIndex = index;
}
}
}
if (foundIndex < 0) {
list.add(item); // add to the end
} else {
list.add(foundIndex, item); // insert at found index
}
}
static Item createPair(Class<?> clazz) {
return new Item(clazz);
}
/** Pair that holds name of a class and maybe the instance.
*/
private static final class Item extends AbstractLookup.Pair<Object> {
/** May be one of three things:
* 1. The implementation class which was named in the services file.
* 2. An instance of it.
* 3. Null, if creation of the instance resulted in an error.
*/
private Object object;
private int position = -1;
public Item(Class<?> clazz) {
this.object = clazz;
}
@Override
public String toString() {
return "MetaInfServicesLookup.Item[" + clazz().getName() + "]"; // NOI18N
}
/** Finds the class.
*/
private Class<? extends Object> clazz() {
Object o = object;
if (o instanceof CantInstantiate) {
return ((CantInstantiate)o).clazz;
}
if (o instanceof Class<?>) {
return (Class<? extends Object>) o;
} else if (o != null) {
return o.getClass();
} else {
// Broken.
return Object.class;
}
}
@Override
public boolean equals(Object o) {
if (o instanceof Item) {
return ((Item) o).clazz().equals(clazz());
}
return false;
}
@Override
public int hashCode() {
return clazz().hashCode();
}
protected @Override boolean instanceOf(Class<?> c) {
return c.isAssignableFrom(clazz());
}
public @Override Class<?> getType() {
return clazz();
}
public @Override Object getInstance() {
Object o = object; // keeping local copy to avoid another
if (o instanceof CantInstantiate) {
return null;
}
// thread to modify it under my hands
if (o instanceof Class<?>) {
synchronized (o) { // o is Class and we will not create
// 2 instances of the same class
Class<?> c = ((Class<?>) o);
try {
o = CACHE.findInstance(c);
if (o == null) {
o = SharedClassObjectBridge.newInstance(c);
// if the instance was created during newInstance call
// and returned, return always the 1st instance, so
// only a single instance is ever observable outside Lookup.
Object other = CACHE.findInstance(c);
if (other != null) {
return object = other;
}
CACHE.storeInstance(o);
}
// Do not assign to instance var unless there is a complete synch
// block between the newInstance and this line. Otherwise we could
// be assigning a half-constructed instance that another thread
// could see and return immediately.
object = o;
} catch (Exception ex) {
LOGGER.log(Level.INFO, "Cannot create " + object, ex);
object = new CantInstantiate(c);
return null;
} catch (LinkageError x) { // #174055 + NoClassDefFoundError
LOGGER.log(Level.FINE, "Cannot create " + object, x); //NOI18N
object = new CantInstantiate(c);
return null;
}
}
}
return object;
}
public @Override String getDisplayName() {
return clazz().getName();
}
public @Override String getId() {
return clazz().getName();
}
protected @Override boolean creatorOf(Object obj) {
return obj == object;
}
}
private static final class CantInstantiate {
final Class<?> clazz;
public CantInstantiate(Class<?> clazz) {
assert clazz != null;
this.clazz = clazz;
}
}
}