blob: 4a6d275a886d67078e904d5702d526ae27640e85 [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.netbeans.modules.editor.mimelookup.impl;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.spi.CustomInstanceFactory;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.Lookups;
/**
*
* @author vita
*/
public final class FolderPathLookup extends AbstractLookup {
/**
* Attribute that points to the original file in datashadow. See RecognizedInstanceFiles
* in openide.filesystems.
*/
private static final String ATTR_ORIGINAL_FILE = "originalFile"; // NOI18N
/**
* Extension of DataShadows, keep in sync with RecognizedInstanceFiles in openide.filesystems
*/
private static final String EXTENSION_SHADOW = "shadow"; // NOI18N
private static final String ATTR_INSTANCE_OF = "instanceOf"; // NOI18N
private static final Logger LOG = Logger.getLogger(FolderPathLookup.class.getName());
private final InstanceContent content;
private final CompoundFolderChildren children;
private final PCL listener = new PCL();
/**
* Map holding fileobject to created instance pairs. Once the clients stop
* referencing the created instance the whole item gets removed from the cache.
* This ensures that the particular file object always gives the same instance
* (e.g. when registered for empty mime-type the same instance gets returned
* when asked for both empty and non-empty mime-types).
* This is crucial for some of the MimeLookup clients.
*/
private static final Map<FileObject,InstanceItem> fo2item = new HashMap<>(128);
static InstanceItem getInstanceItem(FileObject fo, InstanceItem ignoreItem) {
FileObject real = fo;
if (EXTENSION_SHADOW.equals(fo.getExt())) {
Object originalFile = fo.getAttribute(ATTR_ORIGINAL_FILE);
if (originalFile instanceof String) {
FileObject r;
try {
r = fo.getFileSystem().getRoot().getFileObject(originalFile.toString());
if (r != null) {
real = r;
} else {
LOG.warning("Dangling shadow found: " + fo.getPath() + " -> " + originalFile); // NOI18N
}
} catch (FileStateInvalidException ex) {
LOG.log(Level.WARNING, "Unexpected error accessing config file " + fo, ex); // NOI18N
}
}
}
synchronized (fo2item) {
InstanceItem item = fo2item.get(fo);
if (item == null || item == ignoreItem) {
// resolve potential shadows:
item = new InstanceItem(real);
fo2item.put(fo, item);
}
return item;
}
}
static void releaseInstanceItem(InstanceItem item) {
synchronized (fo2item) {
// Optimistically suppose the value in the map is the removed one.
InstanceItem removed = fo2item.remove(item.getFileObject());
if (removed != item) {
fo2item.put(item.getFileObject(), removed);
}
}
}
/** Creates a new instance of InstanceProviderLookup */
public FolderPathLookup(String [] paths) {
this(paths, new InstanceContent());
}
private FolderPathLookup(String [] paths, InstanceContent content) {
super(content);
this.content = content;
this.children = new CompoundFolderChildren(paths, false);
this.children.addPropertyChangeListener(listener);
rebuild();
}
private void rebuild() {
List<PairItem> pairItems = new ArrayList<PairItem>();
for (FileObject fo : children.getChildren()) {
if (!fo.isValid()) {
// Can happen after modules are disabled. Ignore it.
continue;
}
pairItems.add(new PairItem(fo));
}
content.setPairs(pairItems);
}
private class PCL implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
rebuild();
}
} // End of PCL class
private static final class PairItem extends AbstractLookup.Pair<Object> {
private final InstanceItem instanceItem;
PairItem(FileObject fo) {
instanceItem = getInstanceItem(fo, null);
assert (instanceItem != null) : "InstanceItem must not be null";
}
@Override
protected boolean instanceOf(Class<?> c) {
return instanceItem.instanceOf(c);
}
@Override
protected boolean creatorOf(Object obj) {
return instanceItem.creatorOf(obj);
}
@Override
public Object getInstance() {
return instanceItem.getInstance();
}
@Override
public Class<? extends Object> getType() {
return instanceItem.getType();
}
@Override
public String getId() {
return instanceItem.getId();
}
@Override
public String getDisplayName() {
return instanceItem.getDisplayName();
}
@Override
public boolean equals(Object obj) {
// Transferred from RecognizeInstanceFiles.FOItem
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final PairItem other = (PairItem) obj;
return instanceItem.equals(other.instanceItem);
}
@Override
public int hashCode() {
// Transferred from RecognizeInstanceFiles.FOItem
int hash = 3;
hash = 11 * hash + instanceItem.hashCode();
return hash;
}
}
private static volatile Lookup.Result<CustomInstanceFactory> factories;
private static Collection<? extends CustomInstanceFactory> getInstanceFactories() {
Lookup.Result<CustomInstanceFactory> v = factories;
if (v != null) {
return v.allInstances();
}
final Lookup.Result<CustomInstanceFactory>[] fr = new Lookup.Result[1];
// ensure the system - global Lookup is used
Lookups.executeWith(null, new Runnable() {
public void run() {
fr[0] = factories = Lookup.getDefault().lookupResult(CustomInstanceFactory.class);
}
});
return fr[0].allInstances();
}
public static final <T> T createInstance(Class<T> type) throws InstantiationException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
T r = null;
for (CustomInstanceFactory fif : getInstanceFactories()) {
r = fif.createInstance(type);
if (r != null) {
break;
}
}
if (r == null) {
Constructor<T> init = type.getDeclaredConstructor();
init.setAccessible(true);
r = init.newInstance();
}
return r;
}
/**
* Item referencing a file object and object instance that was created from it.
* <br/>
* Once the instance gets released the item will be removed from cache.
*/
private static final class InstanceItem {
static final long serialVersionUID = 10L;
private final FileObject fo;
/** reference to created object */
private transient Reference<Object> ref;
/** Constructs new item. */
InstanceItem (FileObject fo) {
assert (fo != null) : "FileObject must not be null";
this.fo = fo;
}
FileObject getFileObject() {
return fo;
}
private synchronized Reference<Object> getRef() {
return ref;
}
private synchronized void setRef(Reference<Object> ref) {
this.ref = ref;
}
protected boolean instanceOf(Class<?> c) {
Reference<Object> refL = getRef();
Object inst = (refL != null) ? refL.get() : null;
if (inst != null) {
return c.isInstance(inst);
} else {
String instanceOf = (String) fo.getAttribute(ATTR_INSTANCE_OF);
if (instanceOf != null) {
for (String xface : instanceOf.split(",")) {
try {
if (c.isAssignableFrom(Class.forName(xface, false, loader()))) {
return true;
}
} catch (ClassNotFoundException x) {
// Not necessarily a problem, e.g. from org-netbeans-lib-commons_net-antlibrary.instance
LOG.log(Level.FINE, "could not load " + xface + " for " + fo.getPath(), x);
}
}
return false;
} else {
return c.isAssignableFrom(getType());
}
}
}
protected boolean creatorOf(Object obj) {
Reference<Object> refL = getRef();
return (refL != null) ? refL.get() == obj : false;
}
public synchronized Object getInstance() {
Reference<Object> refL = getRef();
Object inst = null;
if (refL != null) {
inst = refL.get();
if (inst == null) { // Instance already released -> get a fresh item
return getInstanceItem(fo, this).getInstance();
}
}
if (inst == null) {
inst = createInstanceFor(fo, Object.class);
if (inst != null) {
setRef(new Ref(inst));
}
}
return inst;
}
public Class<? extends Object> getType() {
Class<? extends Object> type = findTypeFor(fo);
return type != null ? type : Void.class;
}
public String getId() {
String s = fo.getPath();
if (s.endsWith(".instance")) { // NOI18N
s = s.substring(0, s.length() - ".instance".length());
}
return s;
}
public String getDisplayName() {
String n = fo.getName();
try {
n = fo.getFileSystem().getDecorator().annotateName(n, Collections.singleton(fo));
} catch (FileStateInvalidException ex) {
LOG.log(Level.WARNING, ex.getMessage(), ex);
}
return n;
}
public @Override boolean equals(Object obj) {
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final InstanceItem other = (InstanceItem) obj;
if (this.fo != other.fo &&
(this.fo == null || !this.fo.equals(other.fo)))
return false;
return true;
}
public @Override int hashCode() {
return fo.hashCode();
}
private static ClassLoader loader() {
ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
if (l == null) {
l = InstanceItem.class.getClassLoader();
}
return l;
}
static <T> T createInstanceFor(FileObject f, Class<T> resultType) {
Object inst = f.getAttribute("instanceCreate");
if (inst == null) {
try {
Class<?> type = findTypeFor(f);
if (type == null) {
return null;
}
inst = createInstance(type);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
Exceptions.printStackTrace(ex);
}
}
return resultType.isInstance(inst) ? resultType.cast(inst) : null;
}
private static Class<? extends Object> findTypeFor(FileObject f) {
String clazz = getClassName(f);
if (clazz == null) {
return null;
}
try {
return Class.forName(clazz, false, loader());
} catch (ClassNotFoundException ex) {
LOG.log(Level.FINE, ex.getMessage(), ex);
return null;
}
}
/** get class name from specified file object*/
private static String getClassName(FileObject fo) {
// first of all try "instanceClass" property of the primary file
Object attr = fo.getAttribute ("instanceClass");
if (attr instanceof String) {
return BaseUtilities.translate((String) attr);
} else if (attr != null) {
LOG.warning(
"instanceClass was a " + attr.getClass().getName()); // NOI18N
}
attr = fo.getAttribute("instanceCreate");
if (attr != null) {
return attr.getClass().getName();
} else {
Enumeration<String> attributes = fo.getAttributes();
while (attributes.hasMoreElements()) {
if (attributes.nextElement().equals("instanceCreate")) {
// It was specified, just unloadable (usually a methodvalue).
return null;
}
}
}
// otherwise extract the name from the filename
String name = fo.getName ();
int first = name.indexOf('[') + 1;
if (first != 0) {
LOG.log(Level.WARNING, "Cannot understand {0}", fo);
}
int last = name.indexOf (']');
if (last < 0) {
last = name.length ();
}
// take only a part of the string
if (first < last) {
name = name.substring (first, last);
}
name = name.replace ('-', '.');
name = BaseUtilities.translate(name);
return name;
}
void release() {
releaseInstanceItem(this);
}
private final class Ref extends WeakReference<Object> implements Runnable {
Ref(Object inst) {
super(inst, BaseUtilities.activeReferenceQueue());
}
@Override
public void run() {
release();
}
}
}
}