/* * Copyright 2010 Outerthought bvba * * 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.repository.impl; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.lilyproject.repository.api.Blob; import org.lilyproject.repository.api.FieldNotFoundException; import org.lilyproject.repository.api.HierarchyPath; import org.lilyproject.repository.api.IdentityRecordStack; import org.lilyproject.repository.api.Metadata; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.Record; import org.lilyproject.repository.api.RecordException; import org.lilyproject.repository.api.RecordId; import org.lilyproject.repository.api.Repository; import org.lilyproject.repository.api.ResponseStatus; import org.lilyproject.repository.api.Scope; import org.lilyproject.util.ArgumentValidator; import org.lilyproject.util.ObjectUtils; public class RecordImpl implements Record, Cloneable { private RecordId id; private Map<QName, Object> fields = new HashMap<QName, Object>(); private List<QName> fieldsToDelete = new ArrayList<QName>(0); // default size zero because this is used relatively // rarely compared to fields added/updated. private Map<Scope, RecordTypeRef> recordTypes = new EnumMap<Scope, RecordTypeRef>(Scope.class); private Long version; private ResponseStatus responseStatus; private Map<String,String> attributes; private String defaultNamespace = null; private Map<QName, Metadata> metadatas; /** * This constructor should not be called directly. * @use {@link Repository#newRecord} instead */ public RecordImpl() { } /** * This constructor should not be called directly. * @use {@link Repository#newRecord} instead */ public RecordImpl(RecordId id) { this.id = id; } @Override public void setId(RecordId id) { this.id = id; } @Override public RecordId getId() { return id; } @Override public void setVersion(Long version) { this.version = version; } @Override public Long getVersion() { return version; } @Override public void setRecordType(QName name, Long version) { setRecordType(Scope.NON_VERSIONED, name, version); } @Override public void setRecordType(QName name) { setRecordType(name, null); } @Override public QName getRecordTypeName() { return getRecordTypeName(Scope.NON_VERSIONED); } @Override public Long getRecordTypeVersion() { return getRecordTypeVersion(Scope.NON_VERSIONED); } @Override public void setRecordType(Scope scope, QName name, Long version) { if (name == null && version == null) { recordTypes.remove(scope); } else { recordTypes.put(scope, new RecordTypeRef(name, version)); } } @Override public QName getRecordTypeName(Scope scope) { RecordTypeRef ref = recordTypes.get(scope); return ref != null ? ref.name : null; } @Override public Long getRecordTypeVersion(Scope scope) { RecordTypeRef ref = recordTypes.get(scope); return ref != null ? ref.version : null; } @Override public void setField(QName name, Object value) { fields.put(name, value); fieldsToDelete.remove(name); } @Override public <T> T getField(QName name) throws FieldNotFoundException { Object field = fields.get(name); if (field == null) { throw new FieldNotFoundException(name); } return (T)field; } @Override public boolean hasField(QName fieldName) { return fields.containsKey(fieldName); } @Override public Map<QName, Object> getFields() { return fields; } @Override public void delete(QName fieldName, boolean addToFieldsToDelete) { fields.remove(fieldName); if (addToFieldsToDelete) { getFieldsToDelete().add(fieldName); } } @Override public List<QName> getFieldsToDelete() { return fieldsToDelete; } @Override public void addFieldsToDelete(List<QName> names) { if (!names.isEmpty()) { fieldsToDelete.addAll(names); } } @Override public void removeFieldsToDelete(List<QName> names) { fieldsToDelete.removeAll(names); } @Override public ResponseStatus getResponseStatus() { return responseStatus; } @Override public void setResponseStatus(ResponseStatus status) { this.responseStatus = status; } @Override public Record clone() throws RuntimeException { try { return cloneRecord(new IdentityRecordStack()); } catch (RecordException e) { throw new RuntimeException(e); } } @Override public Record cloneRecord() throws RecordException { return cloneRecord(new IdentityRecordStack()); } @Override public Record cloneRecord(IdentityRecordStack parentRecords) throws RecordException { if (parentRecords.contains(this)) { throw new RecordException("A record may not be nested in itself: " + id); } RecordImpl record = new RecordImpl(); record.id = id; record.version = version; record.recordTypes.putAll(recordTypes); parentRecords.push(this); for (Entry<QName, Object> entry : fields.entrySet()) { record.fields.put(entry.getKey(), tryCloneValue(parentRecords, entry)); } parentRecords.pop(); if (fieldsToDelete.size() > 0) { // addAll seems expensive even when list is empty record.fieldsToDelete.addAll(fieldsToDelete); } if (metadatas != null) { for (Map.Entry<QName, Metadata> metadata : metadatas.entrySet()) { record.setMetadata(metadata.getKey(), metadata.getValue()); } } // the ResponseStatus is not cloned, on purpose return record; } private Object tryCloneValue(final IdentityRecordStack parentRecords, final Entry<QName, Object> entry) throws RecordException { try { return cloneValue(entry.getValue(), parentRecords); } catch (CloneNotSupportedException e) { throw new RecordException("Failed to clone record", e); } } private boolean detectRecordRecursion(List<Record> parentRecords) { for (Entry<QName, Object> entry : fields.entrySet()) { if (detectRecordRecursion(entry.getValue(), parentRecords)) { return true; } } return false; } private boolean detectRecordRecursion(Object value, List<Record> parentRecords) { if (value instanceof HierarchyPath) { Object[] elements = ((HierarchyPath) value).getElements(); for (Object object : elements) { if (detectRecordRecursion(object, parentRecords)) { return true; } } } if (value instanceof List) { List<Object> values = (List<Object>) value; for (Object object : values) { if (detectRecordRecursion(object, parentRecords)) { return true; } } } if (value instanceof Record) { if (parentRecords.contains(value)) { return true; } Record record = (Record) value; parentRecords.add(record); Map<QName, Object> fields = record.getFields(); for (Entry<QName, Object> entry : fields.entrySet()) { if (detectRecordRecursion(entry.getValue(), parentRecords)) { return true; } } parentRecords.remove(record); } return false; // Skip all other values } private Object cloneValue(Object value, IdentityRecordStack parentRecords) throws RecordException, CloneNotSupportedException { if (value instanceof HierarchyPath) { Object[] elements = ((HierarchyPath)value).getElements(); Object[] newElements = new Object[elements.length]; for (int i = 0; i < newElements.length; i++) { newElements[i] = cloneValue(elements[i], parentRecords); } return new HierarchyPath(newElements); } if (value instanceof List) { List<Object> newList = new ArrayList<Object>(); List<Object> values = (List<Object>)value; for (Object object : values) { newList.add(cloneValue(object, parentRecords)); } return newList; } if (value instanceof Blob) { return ((Blob)value).clone(); } if (value instanceof Record) { Record record = (Record) value; return (record).cloneRecord(parentRecords); } return value; // All other values are immutable } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((fields == null) ? 0 : fields.hashCode()); result = prime * result + ((fieldsToDelete == null) ? 0 : fieldsToDelete.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((recordTypes == null) ? 0 : recordTypes.hashCode()); result = prime * result + ((version == null) ? 0 : version.hashCode()); return result; } @Override public boolean equals(Object obj) { if (!softEquals(obj)) { return false; } if (obj instanceof RecordRvtImpl) { return equals(((RecordRvtImpl)obj).getRecord()); } if (obj instanceof IdRecordImpl) { return softEquals(((IdRecordImpl)obj).getRecord()); } RecordImpl other = (RecordImpl) obj; if (recordTypes == null) { if (other.recordTypes != null) { return false; } } else if (!recordTypes.equals(other.recordTypes)) { return false; } if (version == null) { if (other.version != null) { return false; } } else if (!version.equals(other.version)) { return false; } return true; } @Override public boolean softEquals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (obj instanceof RecordRvtImpl) { return softEquals(((RecordRvtImpl)obj).getRecord()); } if (obj instanceof IdRecordImpl) { return softEquals(((IdRecordImpl)obj).getRecord()); } if (getClass() != obj.getClass()) { return false; } RecordImpl other = (RecordImpl) obj; if (fields == null) { if (other.fields != null) { return false; } } else if (!fields.equals(other.fields)) { return false; } if (fieldsToDelete == null) { if (other.fieldsToDelete != null) { return false; } } else if (!fieldsToDelete.equals(other.fieldsToDelete)) { return false; } if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } QName nonVersionedRT1 = getRecordTypeName(Scope.NON_VERSIONED); QName nonVersionedRT2 = other.getRecordTypeName(Scope.NON_VERSIONED); if (nonVersionedRT1 != null && nonVersionedRT2 != null && !nonVersionedRT1.equals(nonVersionedRT2)) { return false; } return true; } @Override public String toString() { return "RecordImpl [id=" + id + ", version=" + version + ", recordTypes=" + recordTypes + ", fields=" + fields + ", fieldsToDelete=" + fieldsToDelete + "]"; } private static final class RecordTypeRef { // This object is immutable on purpose (see record clone) final QName name; final Long version; RecordTypeRef(QName name, Long version) { this.name = name; this.version = version; } @Override public String toString() { return name + ":" + version; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((version == null) ? 0 : version.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } RecordTypeRef other = (RecordTypeRef) obj; return ObjectUtils.safeEquals(name, other.name) && ObjectUtils.safeEquals(version, other.version); } } @Override public void setDefaultNamespace(String namespace) { this.defaultNamespace = namespace; } private QName resolveNamespace(String name) throws RecordException { if (defaultNamespace != null) { return new QName(defaultNamespace, name); } QName recordTypeName = getRecordTypeName(); if (recordTypeName != null) { return new QName(recordTypeName.getNamespace(), name); } throw new RecordException("Namespace could not be resolved for name '" + name + "' since no default namespace was given and no record type is set."); } @Override public void setRecordType(String recordTypeName) throws RecordException { setRecordType(resolveNamespace(recordTypeName)); } @Override public void setRecordType(String recordTypeName, Long version) throws RecordException { setRecordType(resolveNamespace(recordTypeName), version); } @Override public void setRecordType(Scope scope, String recordTypeName, Long version) throws RecordException { setRecordType(scope, resolveNamespace(recordTypeName), version); } @Override public <T> T getField(String fieldName) throws FieldNotFoundException, RecordException { // The cast to (T) is only needed for a bug in JDK's < 1.6u24 return (T)getField(resolveNamespace(fieldName)); } @Override public void setField(String fieldName, Object value) throws RecordException { setField(resolveNamespace(fieldName), value); } @Override public void delete(String fieldName, boolean addFieldsToDelete) throws RecordException { delete(resolveNamespace(fieldName), addFieldsToDelete); } @Override public boolean hasField(String fieldName) throws RecordException { return hasField(resolveNamespace(fieldName)); } @Override public Map<String, String> getAttributes() { if (this.attributes == null) { this.attributes = new HashMap<String, String>(); } return this.attributes; } @Override public boolean hasAttributes() { return attributes != null && attributes.size() > 0; } @Override public void setAttributes(Map<String, String> attributes) { this.attributes = attributes; } @Override public Metadata getMetadata(QName fieldName) { if (metadatas == null) { return null; } else { return metadatas.get(fieldName); } } @Override public void setMetadata(QName fieldName, Metadata metadata) { ArgumentValidator.notNull(fieldName, "fieldName"); ArgumentValidator.notNull(metadata, "metadata"); if (metadatas == null) { metadatas = new HashMap<QName, Metadata>(); } metadatas.put(fieldName, metadata); } @Override public Map<QName, Metadata> getMetadataMap() { if (metadatas == null) { return Collections.emptyMap(); } else { return metadatas; } } }