package org.hypergraphdb.type; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.hypergraphdb.HGAtomAttrib; import org.hypergraphdb.HGException; import org.hypergraphdb.HGHandle; import org.hypergraphdb.HGLink; import org.hypergraphdb.HGPersistentHandle; import org.hypergraphdb.HGPlainLink; import org.hypergraphdb.HyperGraph; import org.hypergraphdb.atom.AtomProjection; import org.hypergraphdb.atom.HGSubsumes; import org.hypergraphdb.handle.HGLiveHandle; import org.hypergraphdb.transaction.HGTransaction; import org.hypergraphdb.transaction.TxCacheMap; import org.hypergraphdb.util.HGUtils; import org.hypergraphdb.util.PredefinedTypesConfig; public class JavaTypeSchema implements HGTypeSchema<Class<?>> { private static final int MAX_CLASS_TO_TYPE = 2000; private String predefinedTypes = "/org/hypergraphdb/types"; private HyperGraph graph; private JavaTypeMapper javaTypes = new JavaTypeFactory(); // new DefaultJavaTypeMapper(); // Associates Java class and their corresponding HGDB types. Classes can be // see as identifiers for HGDB Java types and this cache is populated // on a need by need basis by looking up the type system URI->HGHandle // database. But the purpose of this is not only caching since different // class loaders may yield different versions of the same class (as identified // by its fully qualified name). private TxCacheMap<Class<?>, HGHandle> classToAtomType = null; //new ClassToTypeCache(); // per transaction map using during type construction to avoid // circularity in recursive types private static final Map<Class<?>, HGHandle> emptyMap = new HashMap<Class<?>, HGHandle>(); Map<Class<?>, HGHandle> getLocalIdMap() { HGTransaction tx = graph.getTransactionManager().getContext().getCurrent(); if (tx == null) return emptyMap; Map<Class<?>, HGHandle> m = tx.getAttribute(JavaTypeSchema.class.getName() + ".idmap"); if (m == null) { m = new HashMap<Class<?>, HGHandle>(); tx.setAttribute(JavaTypeSchema.class.getName() + ".idmap", m); } return m; } public JavaTypeSchema() { } public JavaTypeSchema(HyperGraph graph) { initialize(graph); } public JavaTypeMapper getJavaTypeFactory() { return javaTypes; } /** * <p>Return the location of the type configuration file. This file can be either * a classpath resource or a file on disk or */ public String getPredefinedTypes() { return predefinedTypes; } /** * <p> * Specify the type configuration file to use when bootstrapping the type system. This file * must contain the list of predefined types needed for the normal functioning of a database * instance. Each line in this text file is a space separated list of (1) the persistent handle * of the type (2) The Java class implementing the {@link HGAtomType} interface and optionally * (3) one or more Java classes to which the type implementation is associated. * </p> * * @param typeConfiguration The location of the type configuration file. First, an attempt * is made to load this location is a classpath resource. Then as a local file. Finally as * a remote URL-based resource. */ public void setPredefinedTypes(String predefinedTypes) { this.predefinedTypes = predefinedTypes; } public String getName() { return "javaclass"; } public boolean isPresent(HyperGraph graph) { return graph.getTypeSystem().getHandleForIdentifier(this.toTypeURI(HGPlainLink.class)) != null; } public void initialize(HyperGraph graph) { this.graph = graph; this.classToAtomType = new TxCacheMap<Class<?>, HGHandle>( graph.getTransactionManager(), ClassToTypeCache.class, this); classToAtomType.load(Top.class, graph.getHandleFactory().topTypeHandle()); // TOP is its own type classToAtomType.load(Object.class, graph.getHandleFactory().topTypeHandle()); // TOP also corresponds to the java.lang.Object "top type" classToAtomType.load(HGLink.class, graph.getHandleFactory().linkTypeHandle()); classToAtomType.load(HGSubsumes.class, graph.getHandleFactory().subsumesTypeHandle()); if (!isPresent(graph)) { PredefinedTypesConfig config = PredefinedTypesConfig.loadFromResource(graph.getHandleFactory(), this.predefinedTypes); for (HGPersistentHandle typeHandle : config.getHandles()) { Class<? extends HGAtomType> cl = config.getTypeImplementation(typeHandle); if (cl.equals(Top.class) || cl.equals(LinkType.class) || cl.equals(SubsumesType.class) || cl.equals(NullType.class)) continue; HGAtomType typeInstance = null; try { typeInstance = cl.newInstance(); } catch (Exception ex) { System.err.println("[HYPERGRAPHDB WARNING]: failed to create instance of type '" + cl.getName() + "'"); ex.printStackTrace(System.err); } List<Class<?>> targets = config.getMappedClasses(typeHandle); if (targets.isEmpty()) graph.getTypeSystem().addPredefinedType(typeHandle, typeInstance, (URI)null); else for (Class<?> target : targets) graph.getTypeSystem().addPredefinedType(typeHandle, typeInstance, target); } } javaTypes.setHyperGraph(graph); // TODO : this is being initialized here because it causes a rather weird issue having to do // with the MVCC implementation if initialized on the spot (the first time it is needed). It // causes the HGPlainLink.class HGDB type to have a null link pointing to it, presumably // HGSubsumes that ends up being null (not in the store). There's some self-referentiality // involved since AtomProjection is a bean, a RecordType and the RecordType implementation // does rely on the AtomProjection type being already defined. But before the MVCC this used // to work without a problem. Anyway, putting the initialization here fixed it, but it might // be just a workaround to a deeper problem that we haven't gotten to the bottom of. --Boris graph.getTypeSystem().getTypeHandle(AtomProjection.class); } public HGAtomType defineType(URI typeId, HGHandle typeHandle, Class<?> descriptor) { return null; } public Class<?> getTypeDescriptor(URI typeId) { try { Class<?> cl = HGUtils.loadClass(graph, uriToClassName(typeId)); if (cl.isPrimitive()) cl = BonesOfBeans.wrapperEquivalentOf(cl); return cl; } catch (ClassNotFoundException e) { throw new HGException(e); } } public HGAtomType toRuntimeType(HGHandle typeHandle, HGAtomType typeInstance) { HGAtomType result = typeInstance; Set<URI> uris = graph.getTypeSystem().getIdentifiersForHandle(typeHandle); //getClassToTypeDB().findFirstByValue(typeHandle); for (URI u : uris) { if (!u.getScheme().equals(this.getName())) continue; String classname = uriToClassName(u); if (classname != null) { Class<?> clazz; try { clazz = HGUtils.loadClass(graph, classname); } catch (ClassNotFoundException e) { // TODO we should have an option that doesn't throw an exception, but just // returns the current type, without wrapping it? throw new HGException(e); } result = javaTypes.getJavaBinding(typeHandle, typeInstance, clazz); classToAtomType.load(clazz, typeHandle); } } return result; } public HGAtomType fromRuntimeType(HGHandle typeHandle, HGAtomType typeInstance) { if (typeInstance instanceof JavaBeanBinding) return ((JavaBeanBinding)typeInstance).getHGType(); else return typeInstance; } public URI toTypeURI(Object object) { return object == null ? null : toTypeURI(object.getClass()); } public HGHandle findType(URI typeId) { return findType(getTypeDescriptor(typeId)); } public HGHandle findType(Class<?> clazz) { Map<Class<?>, HGHandle> m = getLocalIdMap(); HGHandle typeHandle = m.get(clazz); if (typeHandle == null) typeHandle = classToAtomType.get(clazz); if (typeHandle == null) typeHandle = graph.getTypeSystem().getHandleForIdentifier(classNameToURI(clazz.getName())); return typeHandle; } public void removeType(URI typeId) { // We may have different version of the class being loaded by // different class loaders. We need to make sure all version are // removed from the cache since the type is no longer in HGDB. Set<Class<?>> S = new HashSet<Class<?>>(); String clname = uriToClassName(typeId); for (Class<?> cl : classToAtomType.keySet()) if (cl.getName().equals(clname)) S.add(cl); for (Class<?> cl : S) classToAtomType.remove(cl); } public URI toTypeURI(Class<?> javaClass) { return classNameToURI(javaClass.getName()); } public static String uriToClassName(URI uri) { return uri.getRawSchemeSpecificPart(); } public static URI classNameToURI(String classname) { try { return new URI("javaclass:" + classname); } catch (URISyntaxException e) { throw new RuntimeException(e); } } public static URI classToURI(Class<?> cl) { return classNameToURI(cl.getName()); } public void defineType(URI typeId, HGHandle typeHandle) { Class<?> clazz = this.getTypeDescriptor(typeId); Map<Class<?>, HGHandle> m = getLocalIdMap(); m.put(clazz, typeHandle); // // First, create a dummy type for the class, so that recursive type // references don't lead to an infinite recursion here. // graph.define(typeHandle, graph.getHandleFactory().nullTypeHandle(), new Object(), 0); HGHandle inferred = defineNewJavaTypeTransaction(typeHandle, clazz); if (inferred == null) { m.remove(clazz); graph.remove(typeHandle); throw new NoHyperGraphTypeException(typeId); } else { classToAtomType.put(clazz, inferred); } } /** * We need to infer to HG type by introspection. We maintain the * full inheritance tree of Java class and interfaces. Therefore, for each * newly added Java type mapping, we navigate to parent classes etc. */ HGHandle defineNewJavaTypeTransaction(HGHandle newHandle, Class<?> clazz) { // // Create a HyperGraph type matching the Java class. // HGAtomType inferredHGType = javaTypes.defineHGType(clazz, newHandle); if (inferredHGType == null) return null; // throw new HGException("Could not create HyperGraph type for class '" + clazz.getName() + "'"); // // Now, replace the dummy atom that we added at the beginning with the new type. // HGHandle typeConstructor = graph.getTypeSystem().getTypeHandle(inferredHGType.getClass()); if (!typeConstructor.equals(graph.getTypeSystem().getTop())) graph.replace(newHandle, inferredHGType, typeConstructor); // // TODO: we are assuming here that if the defineHGType call above did not return // a newly created type, but an already existing one, its type constructor can // only be Top. There is no reason this should be true in general. Probably // defineHGType should return multiple values: the resulting type, whether it's new // or not and possibly other things, encapsulated in some sort of type descriptor. // else { throw new HGException("Type constructor of newly defined type is TOP, what gives?"); /* // We have a predefined type. We must erase the dummy atom instead of replacing it. graph.remove(newHandle); // A predefined type is a frozen atom in the cache, so its live handle is available // from the Java instance. HGHandle result = graph.getCache().get(inferredHGType); // // Update the Class -> type handle mapping. // classToAtomType.put(clazz, result); getClassToTypeDB().removeEntry(clazz.getName(), graph.getPersistentHandle(newHandle)); getClassToTypeDB().addEntry(clazz.getName(), graph.getPersistentHandle(result)); newHandle = result; */ } // // So far, the inferredHGType is platform independent HyperGraph type, e.g. // a RecordType. To make it transparently handle run-time instances of 'clazz', // we need a corresponding Java binding for this type, e.g. a JavaBeanBinding. // HGAtomType type = javaTypes.getJavaBinding(newHandle, inferredHGType, clazz); type.setHyperGraph(graph); // // the result of hg.add may or may not be stored in the cache: if it is, replace the run-time // instance of // if (! (newHandle instanceof HGLiveHandle) ) // First get the corresponding live handle newHandle = graph.getCache().atomRead((HGPersistentHandle)newHandle, type, new HGAtomAttrib()); newHandle = graph.getCache().atomRefresh((HGLiveHandle)newHandle, type, true); // Now, examine the super type and implemented interfaces // First, make sure we've mapped all interfaces Class<?> [] interfaces = clazz.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { HGHandle interfaceHandle = graph.getTypeSystem().getTypeHandle(interfaces[i]); if (interfaceHandle == null) throw new HGException("Unable to infer HG type for interface " + interfaces[i].getName()); else graph.getTypeSystem().assertSubtype(interfaceHandle, newHandle); } // // Next, navigate to the superclass. // if (clazz.getSuperclass() != null) { HGHandle superHandle = graph.getTypeSystem().getTypeHandle(clazz.getSuperclass()); // Then proceed recursively to the superclass: if (superHandle == null) { throw new HGException("Unable to infer HG type for class " + clazz.getSuperclass().getName() + " the superclass of " + clazz.getName()); } else graph.getTypeSystem().assertSubtype(superHandle, newHandle); } // Interfaces don't derive from java.lang.Object, so we need to super-type them with Top explicitly else if (clazz.isInterface()) graph.add(new HGSubsumes(graph.getTypeSystem().getTop(), newHandle)); // // ouf, we're done // return newHandle; } public class ClassToTypeCache extends LinkedHashMap<Class<?>, HGHandle> { static final long serialVersionUID = -1; @SuppressWarnings("unused") public ClassToTypeCache() { super(1000, 0.75f, true); } protected boolean removeEldestEntry(Map.Entry<Class<?>, HGHandle> eldest) { if (size() > MAX_CLASS_TO_TYPE) { if (eldest.getValue() instanceof HGLiveHandle) { HGLiveHandle h = (HGLiveHandle)eldest.getValue(); if (h.getRef() == null) return true; //if it has been evicted from the atom cache, removed it from here too else if (graph.getCache().isFrozen(h)) return get(eldest.getKey()) == null; // this will return false and put the element on top of the list else return false; // simply return false, but don't remove since it's still in the cache } else { HGLiveHandle h = graph.getCache().get((HGPersistentHandle)eldest.getValue()); if (h != null) { eldest.setValue(h); return false; } else return true; } } else return false; } } }