/* * Copyright 2012 NGDATA nv * * Licensed 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.lilyproject.util.repo; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.apache.commons.logging.LogFactory; import org.lilyproject.repository.api.FieldType; import org.lilyproject.repository.api.FieldTypeNotFoundException; import org.lilyproject.repository.api.IdGenerator; import org.lilyproject.repository.api.Link; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.Record; import org.lilyproject.repository.api.RecordType; import org.lilyproject.repository.api.RepositoryException; import org.lilyproject.repository.api.SchemaId; import org.lilyproject.repository.api.Scope; import org.lilyproject.repository.api.TypeManager; import org.lilyproject.repository.api.ValueType; /** * The idea behind SystemFields is to make system properties of records addressable as normal fields, * that is, to make them addressable by a QName. This way, both fields and system properties are * accessible through a uniform interface. This avoids the need to have two different addressing * systems in situations that want to provide access to both. * * <p>The namespace for all system fields is org.lilyproject.system</p> * * <p>At the time of this writing, SystemFields is used by the indexer and by conditional * record updates.</p> * * <p>It was not the intention to make the abstraction work the whole way, i.e. Record objects are not * decorated to make the system fields appear, nor are they reported as 'changed fields' in the record events, etc. * It seemed this could have all sorts of undesirable side-effects. * <p>For each of the supported system fields, corresponding 'fake' FieldType objects are available. * The UUIDs of these field type objects are name-based UUIDs, so they should never collide with those * generated by Lily. * * <p>The FieldType objects returned by this class are not clones, so be careful not to modify them. */ public class SystemFields { public static final String NS = "org.lilyproject.system"; private static final SystemField[] fields = new SystemField[] { new SystemField("version", "LONG", false) { @Override public Object eval(Record record, TypeManager typeManager) { // Special about the version field is that it can evaluate to null, while normal fields // will never be null. Or should we rather throw a FieldNotFoundException when its null? return record.getVersion(); } }, new SystemField("recordType", "STRING", false) { @Override public Object eval(Record record, TypeManager typeManager) { return formatName(record.getRecordTypeName()); } }, new SystemField("recordTypeName", "STRING", false) { @Override public Object eval(Record record, TypeManager typeManager) { return record.getRecordTypeName().getName(); } }, new SystemField("recordTypeNamespace", "STRING", false) { @Override public Object eval(Record record, TypeManager typeManager) { return record.getRecordTypeName().getNamespace(); } }, new SystemField("recordTypeVersion", "LONG", false) { @Override public Object eval(Record record, TypeManager typeManager) { return record.getRecordTypeVersion(); } }, new SystemField("recordTypeWithVersion", "STRING", false) { @Override public Object eval(Record record, TypeManager typeManager) { return formatNameVersion(record.getRecordTypeName(), record.getRecordTypeVersion()); } }, new SystemField("supertypes", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatName(recordType.getName())); } }); return result; } }, new SystemField("supertypesWithVersion", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatNameVersion(recordType.getName(), recordType.getVersion())); } }); return result; } }, new SystemField("supertypeNames", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getName()); } }); return result; } }, new SystemField("supertypeNamespaces", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getNamespace()); } }); return result; } }, /* The mixin related fields were deprecated in 2.2, they can be removed in 2.4 */ new SystemField("mixins", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { LogFactory.getLog("lily.deprecation") .warn("System field mixins is deprecated, use supertypes instead."); final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatName(recordType.getName())); } }); return result; } }, new SystemField("mixinsWithVersion", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { LogFactory.getLog("lily.deprecation") .warn("System field mixinsWithVersion is deprecated, use supertypesWithVersion instead."); final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatNameVersion(recordType.getName(), recordType.getVersion())); } }); return result; } }, new SystemField("mixinNames", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { LogFactory.getLog("lily.deprecation") .warn("System field mixinNames is deprecated, use supertypeNames instead."); final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getName()); } }); return result; } }, new SystemField("mixinNamespaces", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { LogFactory.getLog("lily.deprecation") .warn("System field mixinNamespaces is deprecated, use supertypeNamespaces instead."); final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, false, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getNamespace()); } }); return result; } }, new SystemField("recordTypes", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, true, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatName(recordType.getName())); } }); return result; } }, new SystemField("recordTypesWithVersion", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, true, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(formatNameVersion(recordType.getName(), recordType.getVersion())); } }); return result; } }, new SystemField("recordTypeNames", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, true, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getName()); } }); return result; } }, new SystemField("recordTypeNamespaces", "STRING", true) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { final List<String> result = new NoDupsList<String>(); forEachSupertype(record, typeManager, true, new SupertypeCallback() { @Override public void handle(RecordType recordType) { result.add(recordType.getName().getNamespace()); } }); return result; } }, new SystemField("id", "STRING", false) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { return record.getId(); } }, new SystemField("link", "LINK", false) { @Override public Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException { return new Link(record.getId()); } } }; private static SystemFields INSTANCE; private Map<QName, SystemField> fieldsByName; private Map<SchemaId, SystemField> fieldsById; public SystemFields(Map<QName, SystemField> fieldsByName, Map<SchemaId, SystemField> fieldsById) { this.fieldsByName = fieldsByName; this.fieldsById = fieldsById; } public static synchronized SystemFields getInstance(TypeManager typeManager, IdGenerator idGenerator) { if (INSTANCE == null) { Map<QName, SystemField> fieldsByName = new HashMap<QName, SystemField>(); Map<SchemaId, SystemField> fieldsById = new HashMap<SchemaId, SystemField>(); for (SystemField field : fields) { String stringId = "{" + NS + "}" + field.name; UUID id; try { // Normally, the bytes should be prefixed with the UUID of the namespace to which the name // belongs, but for our purpose this is a good enough. id = UUID.nameUUIDFromBytes(stringId.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); // rare enough } SchemaId schemaId = idGenerator.getSchemaId(id); ValueType valueType; try { valueType = field.multiValue ? typeManager.getValueType("LIST<"+field.type+">") : typeManager.getValueType(field.type); } catch (RepositoryException e) { throw new RuntimeException(e); // unlikely to occur } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); // unlikely to occur } FieldType fieldType = typeManager.newFieldType(valueType, new QName(NS, field.name), Scope.NON_VERSIONED); fieldType.setId(schemaId); field.setFieldType(fieldType); fieldsByName.put(fieldType.getName(), field); fieldsById.put(fieldType.getId(), field); } INSTANCE = new SystemFields(fieldsByName, fieldsById); } return INSTANCE; } public boolean isSystemField(QName name) { return fieldsByName.containsKey(name); } public boolean isSystemField(SchemaId schemaId) { return fieldsById.containsKey(schemaId); } public FieldType get(QName name) throws FieldTypeNotFoundException { if (!fieldsByName.containsKey(name)) { throw new FieldTypeNotFoundException(name); } return fieldsByName.get(name).fieldType; } public FieldType get(SchemaId schemaId) throws FieldTypeNotFoundException { if (!fieldsById.containsKey(schemaId)) { throw new FieldTypeNotFoundException(schemaId); } return fieldsById.get(schemaId).fieldType; } public Object eval(Record record, FieldType fieldType, TypeManager typeManager) throws RepositoryException, InterruptedException { return fieldsById.get(fieldType.getId()).eval(record, typeManager); } /** * If it is a system field, evaluates it, if not, returns the normal field value from the record. * Does not throw a FieldNotFoundException, rather returns null. */ public Object softEval(Record record, QName fieldType, TypeManager typeManager) throws RepositoryException, InterruptedException { if (isSystemField(fieldType)) { return fieldsByName.get(fieldType).eval(record, typeManager); } else { return record.getFields().get(fieldType); } } public Set<QName> getAll() { return fieldsByName.keySet(); } private static abstract class SystemField { private String name; private String type; private boolean multiValue; private FieldType fieldType; SystemField(String name, String type, boolean multiValue) { this.name = name; this.type = type; this.multiValue = multiValue; } public void setFieldType(FieldType fieldType) { this.fieldType = fieldType; } public abstract Object eval(Record record, TypeManager typeManager) throws RepositoryException, InterruptedException; } private static String formatName(QName name) { return "{" + name.getNamespace() + "}" + name.getName(); } private static String formatNameVersion(QName name, long version) { return "{" + name.getNamespace() + "}" + name.getName() + ":" + version; } private static interface SupertypeCallback { void handle(RecordType recordType); } private static void forEachSupertype(Record record, TypeManager typeManager, boolean includeRecordType, SupertypeCallback callback) throws RepositoryException, InterruptedException { RecordType recordType = typeManager.getRecordTypeByName(record.getRecordTypeName(), record.getRecordTypeVersion()); if (includeRecordType) { callback.handle(recordType); } for (Map.Entry<SchemaId, Long> supertype : recordType.getSupertypes().entrySet()) { RecordType supertypeRt = typeManager.getRecordTypeById(supertype.getKey(), supertype.getValue()); callback.handle(supertypeRt); } } /** * Extends list to avoid that the same item is added twice (only for plain add). For the small lists we * have, assumed this would be cheaper than first constructing a set and then converting it to a list. */ private static class NoDupsList<T> extends ArrayList<T> { @Override public boolean add(T t) { if (!this.contains(t)) { return super.add(t); } return false; } } }