/* * Copyright 2009-2016 Tilmann Zaeschke. All rights reserved. * * This file is part of ZooDB. * * ZooDB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ZooDB is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ZooDB. If not, see <http://www.gnu.org/licenses/>. * * See the README and COPYING files for further information. */ package org.zoodb.internal; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.zoodb.api.impl.ZooPC; import org.zoodb.internal.ZooFieldDef.JdoType; import org.zoodb.internal.client.PCContext; import org.zoodb.internal.client.SchemaOperation; import org.zoodb.internal.client.session.ClientSessionCache; import org.zoodb.internal.util.DBLogger; import org.zoodb.internal.util.Util; /** * ZooClassDef represents a class schema definition used by the database. * The highest stored schema is that of PersistenceCapableImpl. * * Initialization takes three(four) steps: * 1) Instance creation and association with super OID. * 2) Association with super class (this may not be instantiated during 1). * 3) Initialization of fields and their offsets. This requires a complete inheritance hierarchy. * 4) Update FCO fields with ClassDef OIDs. * * @author Tilmann Zaeschke */ public class ZooClassDef extends ZooPC { private String className; private transient Class<?> cls; private long oidSuper; //This is a unique Schema ID. All versions of a schema have the same ID. private final long schemaId; private final short versionId; private transient ZooClassDef superDef; private transient ZooClassProxy versionProxy = null; private final ArrayList<ZooFieldDef> localFields = new ArrayList<ZooFieldDef>(10); private transient ZooFieldDef[] allFields = new ZooFieldDef[0]; private transient HashMap<String, ZooFieldDef> fieldBuffer = null; private transient PCContext providedContext = null; private long prevVersionOid = 0; private transient ZooClassDef nextVersion = null; private transient ZooClassDef prevVersion = null; //Indicates whether the class is schema-compatible with the Java class of the same name private transient boolean isJavaCompatible = false; //List of operations that transform a previous version into the current version. private ArrayList<PersistentSchemaOperation> evolutionOperations = null; private ZooClassDef() { //DO not use, for de-serializer only! oidSuper = 0; schemaId = 0; versionId = -1; } private ZooClassDef(String clsName, long oid, long superOid, long schemaId, int versionId) { jdoZooSetOid(oid); this.className = clsName; this.oidSuper = superOid; this.schemaId = schemaId; this.versionId = (short) versionId; } /** * Methods used for bootstrapping the schema of newly created databases. * @return Root schema */ public static ZooClassDef bootstrapZooPCImpl() { //The bootstrapped schemata have a fixed OID. //This is because they also need to be created every time we open a database. //They are required to actually read the boot-schema from the database .... //Actually, we don't really need to read them, but they shold be in the database //anyway, for consistency. //TODO maybe we don't need to store ZooClassDef???? // -> and ZooPC does not need to be bootstrapped in memory???? ZooClassDef x = new ZooClassDef(ZooPC.class.getName(), 50, 0, 50, 0); x.cls = ZooPC.class; x.className = ZooPC.class.getName(); //x.associateFields(); //doesn't seem to be necessary return x; } /** * Methods used for bootstrapping the schema of newly created databases. * @return Meta schema instance */ public static ZooClassDef bootstrapZooClassDef() { ZooClassDef meta = new ZooClassDef(ZooClassDef.class.getName(), 51, 50, 51, 0); ArrayList<ZooFieldDef> fields = new ArrayList<ZooFieldDef>(); fields.add(new ZooFieldDef(meta, "className", String.class.getName(), 0, JdoType.STRING, 70)); fields.add(new ZooFieldDef(meta, "oidSuper", long.class.getName(), 0, JdoType.PRIMITIVE, 71)); fields.add(new ZooFieldDef(meta, "schemaId", long.class.getName(), 0, JdoType.PRIMITIVE, 72)); fields.add(new ZooFieldDef(meta, "versionId", short.class.getName(), 0, JdoType.PRIMITIVE, 73)); fields.add(new ZooFieldDef(meta, "localFields", ArrayList.class.getName(), 0, JdoType.SCO, 74)); fields.add(new ZooFieldDef(meta, "prevVersionOid", long.class.getName(), 0, JdoType.PRIMITIVE, 75)); fields.add(new ZooFieldDef(meta, "evolutionOperations", ArrayList.class.getName(), 0, JdoType.SCO, 76)); //new ZooFieldDef(this, allFields, ZooFieldDef[].class.getName(), typeOid, JdoType.ARRAY); meta.registerFields(fields); meta.cls = ZooClassDef.class; meta.className = ZooClassDef.class.getName(); meta.associateFields(); meta.associateJavaTypes(); return meta; } public ZooClassDef getModifiableVersion(ClientSessionCache cache, List<SchemaOperation> ops) { return getModifiableVersion(cache, ops, null); } public ZooClassDef getModifiableVersion(ClientSessionCache cache, List<SchemaOperation> ops, ZooClassDef newSuper) { if (this.jdoZooIsNew()) { //this happens for example when the super-class is modified AFTER the local class got //a new version. //In other words: //First we add all sub-classes. This is important, because getModifiableVersion() only //add a new version to the super-class if it actually creates a new version. If there //is already a modifiable sub-class, it would not be added. ensureLatestSuper(); return this; } ZooClassDef defNew = newVersion(cache, newSuper); ops.add(new SchemaOperation.SchemaNewVersion(this, defNew, cache)); for (ZooClassProxy sub: versionProxy.getSubProxies()) { //ensure that all sub-classes become modifiable versions. sub.getSchemaDef().getModifiableVersion(cache, ops, defNew); } return defNew; } /** * Schema versioning: We only create new schema instance when we add or remove fields. * Renaming a field should not result in a new version! * A new version is only required when the modified schema does not match the stored data. Such * changes require also new versions of all sub-classes. * WHY? If every class stored only their own fields would we still have a problem? Yes, * because the new version of the referenced superclass has a different OID. * @param cache * * @return New version. */ private ZooClassDef newVersion(ClientSessionCache cache, ZooClassDef newSuper) { if (nextVersion != null) { throw new IllegalStateException(); } if (newSuper == null) { //no new version of super available newSuper = superDef; } long oid = jdoZooGetContext().getNode().getOidBuffer().allocateOid(); ZooClassDef newDef = new ZooClassDef(className, oid, newSuper.getOid(), schemaId, versionId + 1); //super-class newDef.associateSuperDef(newSuper); //caches cache.addSchema(newDef, false, jdoZooGetContext().getNode()); //versions newDef.prevVersionOid = jdoZooGetOid(); newDef.prevVersion = this; nextVersion = newDef; //API class newDef.versionProxy = versionProxy; versionProxy.newVersion(newDef); //context newDef.providedContext = new PCContext(newDef, providedContext.getSession(), providedContext.getNode()); //fields for (ZooFieldDef f: localFields) { ZooFieldDef fNew = new ZooFieldDef(f, newDef); newDef.localFields.add(fNew); if (fNew.getProxy() != null) { fNew.getProxy().updateVersion(fNew); } } newDef.associateFields(); return newDef; } public void ensureLatestSuper() { //This is also the general population function for the sub-class list if (superDef.getNextVersion() != null) { superDef = superDef.getNextVersion(); //This is the only place where we may change the super-oid, because the local class is //modifiable and the super-class becomes modifiable as well. oidSuper = superDef.getOid(); } if (superDef.getNextVersion() != null) { throw new IllegalStateException(); } } public ZooClassDef newVersionRollback(ZooClassDef newDef, ClientSessionCache cache) { if (nextVersion != newDef) { throw new IllegalStateException(); } if (newDef.nextVersion != null) { throw new IllegalStateException(); } //caches newDef.jdoZooMarkDeleted(); cache.updateSchema(this, newDef.getJavaClass(), this.getJavaClass()); //versions nextVersion = null; //API class versionProxy = newDef.versionProxy; versionProxy.newVersionRollback(newDef); //fields for (ZooFieldDef f: localFields) { if (f.getProxy() != null) { f.getProxy().updateVersion(f); } } //sub-classes are initialised via ensureLatestSuper(). return newDef; } public static ZooClassDef declare(String clsName, long oid, long superOid) { return new ZooClassDef(clsName, oid, superOid, oid, 0); } public static ZooClassDef createFromJavaType(Class<?> cls, ZooClassDef defSuper, Node node, Session session) { //create instance ZooClassDef def; long superOid = 0; if (cls != ZooPC.class) { if (defSuper == null) { throw DBLogger.newUser("Super class is not persistent capable: " + cls); } superOid = defSuper.getOid(); if (superOid == 0) { throw DBLogger.newFatalInternal("No super class found: " + cls.getName()); } } long oid = node.getOidBuffer().allocateOid(); def = new ZooClassDef(cls.getName(), oid, superOid, oid, 0); //local fields: ArrayList<ZooFieldDef> fieldList = new ArrayList<ZooFieldDef>(); Field[] fields = cls.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field jField = fields[i]; if (Modifier.isStatic(jField.getModifiers()) || Modifier.isTransient(jField.getModifiers())) { continue; } //we cannot set references to other ZooClassDefs yet, as they may not be made //persistent yet long fieldOid = node.getOidBuffer().allocateOid(); ZooFieldDef zField = ZooFieldDef.createFromJavaType(def, jField, fieldOid); fieldList.add(zField); } // init class def.registerFields(fieldList); def.cls = cls; def.associateSuperDef(defSuper); def.associateProxy(new ZooClassProxy(def, session)); def.associateFields(); return def; } public void initProvidedContext(Session session, Node node) { if (providedContext != null) { if (!className.equals(ZooClassDef.class.getName())) { throw new IllegalStateException(className); } else { //ignore resetting attempt for ZooClassDef return; } } providedContext = new PCContext(this, session, node); } /** * * @return The context that this classDef provides for instances of its class. */ public final PCContext getProvidedContext() { return providedContext; } private void registerFields(List<ZooFieldDef> fieldList) { localFields.addAll(fieldList); } public void associateFCOs(Collection<ZooClassDef> cachedSchemata, boolean isSchemaAutoCreateMode, Set<String> missingSchemas) { //Fields: for (ZooFieldDef zField: localFields) { if (zField.isPrimitiveType()) { //no further work for primitives continue; } ZooClassDef typeDef = null; typeDef = zField.getType(); if (typeDef != null) { //do we need to find the latest type? I think so..., if the type has been //renamed AND modified... while (typeDef.getNextVersion() != null) { typeDef = typeDef.getNextVersion(); } } if (typeDef == null) { String typeName = zField.getTypeName(); for (ZooClassDef cs: cachedSchemata) { if (cs.getClassName().equals(typeName)) { typeDef = cs; break; } } } if (typeDef == null) { if (zField.getJdoType() == JdoType.REFERENCE) { if (isSchemaAutoCreateMode) { missingSchemas.add(zField.getTypeName()); continue; } String typeName = zField.getTypeName(); throw DBLogger.newUser("Schema error, class " + getClassName() + " references " + "class " + typeName + " as embedded object, but embedded objects " + "of this type are not allowed. If it extend ZooPC or " + "PersistenceCapableImpl then it should have its own schema defined."); } //found SCO continue; } if (typeDef.jdoZooIsDeleted()) { throw DBLogger.newUser("Schema error, class " + getClassName() + " references " + "class " + typeDef.getClassName() + ", but this has been deleted."); } //This is actually only necessary if the type is not yet assigned. //It may already be assigned in cases where the type has been renamed. zField.setType(typeDef); } } public String getClassName() { return className; } public long getOid() { return jdoZooGetOid(); } public Class<?> getJavaClass() { return cls; } public void associateJavaTypes() { associateJavaTypes(false); } public void associateJavaTypes(boolean failForMismatch) { if (cls != null) { if (!className.equals(ZooClassDef.class.getName()) && !className.equals(ZooPC.class.getName())) { System.out.println("This is new, FIX this!"); //TODO remove return; //throw new IllegalStateException(cls.getName()); } } if (nextVersion != null) { //Java classes are unlikely to fit with outdated schemas //TODO check this, we could still set this, this would avoid the Deserializer //to have find the latest version when following a reference to an instance //of an evolved class. return; } String fName = null; try { Class<?> tmpClass = Class.forName(className); for (ZooFieldDef f: localFields) { fName = f.getName(); Field jf = tmpClass.getDeclaredField(fName); //this may fail due to incompatibilities! f.setJavaField(jf); } isJavaCompatible = true; cls = tmpClass; } catch (ClassNotFoundException e) { //okay we will use artifical/generic classes return; } catch (SecurityException e) { throw DBLogger.newFatal("No access to class fields: " + className + "." + fName, e); } catch (NoSuchFieldException e) { //okay, Java class is incompatible. We continue anyway, but ensure that the //Java deserializer is not used. for (ZooFieldDef f: localFields) { f.unsetJavaField(); } return; } // We check field mismatches and missing Java fields above. // Now check field count, this should cover missing schema fields (too many Java fields). // we need to filter out transient and static fields int n = 0; for (Field f: cls.getDeclaredFields()) { int mod = f.getModifiers(); if (Modifier.isTransient(mod) || Modifier.isStatic(mod)) { continue; } n++; } if (localFields.size() != n) { cls = null; if (failForMismatch) { throw DBLogger.newUser("Schema error, field count mismatch between Java class (" + n + ") and database class (" + localFields.size() + ")."); } } } public ArrayList<ZooFieldDef> getLocalFields() { return localFields; } public ZooFieldDef[] getAllFields() { return allFields; } public ZooClassProxy getVersionProxy() { return versionProxy; } public long getSuperOID() { return oidSuper; } public ZooClassDef getSuperDef() { return superDef; } public void associateVersions(Map<Long, ZooClassDef> schemata) { if (prevVersionOid != 0) { prevVersion = schemata.get(prevVersionOid); prevVersion.nextVersion = this; } } /** * Only to be used during database startup to load the schema-tree. * @param superDef */ public void associateSuperDef(ZooClassDef superDef) { if (this.superDef != null) { throw new IllegalStateException(); } if (superDef == null) { throw new IllegalArgumentException(); } //class invariant if (superDef.getOid() != oidSuper) { throw new IllegalStateException("s-oid= " + oidSuper + " / " + superDef.getOid() + " class=" + className); } this.superDef = superDef; } public void associateFields() { ArrayList<ZooFieldDef> allFields = new ArrayList<ZooFieldDef>(); //For PersistenceCapableImpl _super may be null: ZooClassDef sup = superDef; while (sup != null) { allFields.addAll(0, sup.getLocalFields()); sup = sup.superDef; } int ofs = ZooFieldDef.OFS_INIITIAL; //8 + 8; //Schema-OID + OID if (!allFields.isEmpty()) { ofs = allFields.get(allFields.size()-1).getNextOffset(); } //local fields: int i = allFields.size(); for (ZooFieldDef f: localFields) { f.setOffset(ofs, i); ofs = f.getNextOffset(); allFields.add(f); i++; } this.allFields = allFields.toArray(new ZooFieldDef[allFields.size()]); } public ZooFieldDef getField(String attrName) { for (ZooFieldDef f: allFields) { if (f.getName().equals(attrName)) { return f; } } throw DBLogger.newUser("Field name not found: " + attrName); } public Map<String, ZooFieldDef> getAllFieldsAsMap() { if (fieldBuffer == null) { fieldBuffer = new HashMap<String, ZooFieldDef>(); for (ZooFieldDef def: getAllFields()) { fieldBuffer.put(def.getName(), def); } } return fieldBuffer; } public boolean hasSuperClass(ZooClassDef cls) { if (superDef == cls) { return true; } if (superDef == null) { return false; } return superDef.hasSuperClass(cls); } @Override public String toString() { return className + " (" + Util.oidToString(getOid()) + ") super=" + superDef; } public void rename(String newName) { zooActivateWrite(); className = newName; cls = null; fieldBuffer = null; associateJavaTypes(); } public void addField(ZooFieldDef field) { localFields.add(field); rebuildFieldsRecursive(); newEvolutionOperationAdd(allFields.length-1, null); } void rebuildFieldsRecursive() { associateFields(); for (ZooClassProxy c: versionProxy.getSubProxies()) { c.getSchemaDef().rebuildFieldsRecursive(); } } public void removeField(ZooFieldDef fieldDef) { int i = 0; // remove from localFields for (ZooFieldDef fd: localFields) { if (fd.getName().equals(fieldDef.getName())) { localFields.remove(i); break; } i++; } // for op, use position in allFields i = 0; for (ZooFieldDef fd: allFields) { if (fd.getName().equals(fieldDef.getName())) { rebuildFieldsRecursive(); newEvolutionOperationRemove(i); return; } i++; } throw new IllegalStateException("Field not found: " + fieldDef); } public ZooClassDef getNextVersion() { return nextVersion; } public ZooClassDef getPreviousVersion() { return prevVersion; } private void newEvolutionOperation(PersistentSchemaOperation op) { if (evolutionOperations == null) { evolutionOperations = new ArrayList<PersistentSchemaOperation>(); } evolutionOperations.add(op); } private void newEvolutionOperationAdd(int fieldId, Object initialValue) { newEvolutionOperation(PersistentSchemaOperation.newAddOperation( fieldId, allFields[fieldId], initialValue)); for (ZooClassProxy sub: versionProxy.getSubProxies()) { sub.getSchemaDef().newEvolutionOperationAdd(fieldId, initialValue); } } private void newEvolutionOperationRemove(int fieldId) { newEvolutionOperation(PersistentSchemaOperation.newRemoveOperation(fieldId)); for (ZooClassProxy sub: versionProxy.getSubProxies()) { sub.getSchemaDef().newEvolutionOperationRemove(fieldId); } } /** * The List of evolution operations contains all operations that are required to turn a * previous schema version into the present schema version. This includes also operations * on super-classes. Field-IDs are relative to the allFields[]. * * @return List of operations */ public List<PersistentSchemaOperation> getEvolutionOps() { return evolutionOperations; } /** * Returns the unique schema ID which is independent of the schema version. */ public long getSchemaId() { return schemaId; } /** * Returns the version number of this schema version, starting with 0. */ public int getSchemaVersion() { return versionId; } public void associateProxy(ZooClassProxy px) { if (versionProxy != null) { throw new IllegalStateException(); } versionProxy = px; } /** * * @param def * @return True if this class is the same or a super-type of 'def'. Otherwise returns false. */ public boolean isSuperTypeOf(ZooClassDef def) { if (this == def) { return true; } if (def.superDef == null) { return false; } return isSuperTypeOf(def.superDef); } }