/*
* 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.valuetype;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.lilyproject.bytes.api.DataInput;
import org.lilyproject.bytes.api.DataOutput;
import org.lilyproject.bytes.impl.DataOutputImpl;
import org.lilyproject.repository.api.FieldType;
import org.lilyproject.repository.api.FieldTypeEntry;
import org.lilyproject.repository.api.IdentityRecordStack;
import org.lilyproject.repository.api.InvalidRecordException;
import org.lilyproject.repository.api.QName;
import org.lilyproject.repository.api.Record;
import org.lilyproject.repository.api.RecordException;
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;
import org.lilyproject.repository.api.ValueTypeFactory;
import org.lilyproject.repository.impl.IdRecordImpl;
import org.lilyproject.repository.impl.RecordImpl;
import org.lilyproject.repository.impl.RecordRvtImpl;
import org.lilyproject.repository.impl.id.SchemaIdImpl;
public class RecordValueType extends AbstractValueType implements ValueType {
public static final String NAME = "RECORD";
private String fullName;
private static final byte ENCODING_VERSION = (byte)1;
private static final byte UNDEFINED = (byte)0;
private static final byte DEFINED = (byte)1;
private final TypeManager typeManager;
private QName valueTypeRecordTypeName = null;
public RecordValueType(TypeManager typeManager, String recordTypeName) throws IllegalArgumentException,
RepositoryException, InterruptedException {
this.typeManager = typeManager;
if (recordTypeName != null) {
this.valueTypeRecordTypeName = QName.fromString(recordTypeName);
this.fullName = NAME + "<" + recordTypeName + ">";
} else {
this.fullName = NAME;
}
}
@Override
public String getBaseName() {
return NAME;
}
@Override
public String getName() {
return fullName;
}
@Override
public ValueType getDeepestValueType() {
return this;
}
@Override
@SuppressWarnings("unchecked")
public Record read(byte[] data) throws RepositoryException, InterruptedException {
return new RecordRvtImpl(data, this);
}
@Override
@SuppressWarnings("unchecked")
public Record read(DataInput dataInput) throws RepositoryException, InterruptedException {
Record record = new RecordImpl();
dataInput.readByte(); // Ignore, there is currently only one encoding : 1
int length = dataInput.readVInt();
byte[] recordTypeId = dataInput.readBytes(length);
Long recordTypeVersion = dataInput.readLong();
RecordType recordType = typeManager.getRecordTypeById(new SchemaIdImpl(recordTypeId), recordTypeVersion);
record.setRecordType(recordType.getName(), recordTypeVersion);
Map<SchemaId, QName> idToQNameMapping = new HashMap<SchemaId, QName>();
List<FieldType> fieldTypes = getSortedFieldTypes(recordType);
for (FieldType fieldType : fieldTypes) {
byte readByte = dataInput.readByte();
if (DEFINED == readByte) {
Object value = fieldType.getValueType().read(dataInput);
record.setField(fieldType.getName(), value);
idToQNameMapping.put(fieldType.getId(), fieldType.getName());
}
}
Map<Scope, SchemaId> recordTypeIds = new EnumMap<Scope, SchemaId>(Scope.class);
recordTypeIds.put(Scope.NON_VERSIONED, recordType.getId());
return new IdRecordImpl(record, idToQNameMapping, recordTypeIds);
}
@Override
public byte[] toBytes(Object value, IdentityRecordStack parentRecords) throws RepositoryException,
InterruptedException {
if (value instanceof RecordRvtImpl) {
byte[] bytes = ((RecordRvtImpl)value).getBytes();
if (bytes != null) {
return bytes;
}
}
DataOutput dataOutput = new DataOutputImpl();
encodeData(value, dataOutput, parentRecords);
return dataOutput.toByteArray();
}
@Override
public void write(Object value, DataOutput dataOutput, IdentityRecordStack parentRecords)
throws RepositoryException, InterruptedException {
if (value instanceof RecordRvtImpl) {
byte[] bytes = ((RecordRvtImpl)value).getBytes();
if (bytes != null) {
dataOutput.writeBytes(bytes);
return;
}
}
encodeData(value, dataOutput, parentRecords);
}
private void encodeData(Object value, DataOutput dataOutput, IdentityRecordStack parentRecords)
throws RepositoryException, InterruptedException {
Record record = (Record)value;
if (parentRecords.contains(record)) {
throw new RecordException("A record may not be nested in itself: " + record.getId());
}
RecordType recordType;
QName recordRecordTypeName = record.getRecordTypeName();
if (recordRecordTypeName != null) {
if (valueTypeRecordTypeName != null) {
// Validate the same record type is being used
// 20130314: temporarily disabled this, see LILY-1279
// if (!valueTypeRecordTypeName.equals(recordRecordTypeName)) {
// throw new RecordException("The record's Record Type '" + recordRecordTypeName +
// "' does not match the record value type's record type '" + valueTypeRecordTypeName + "'");
// }
}
recordType = typeManager.getRecordTypeByName(recordRecordTypeName, null);
} else if (valueTypeRecordTypeName != null) {
recordType = typeManager.getRecordTypeByName(valueTypeRecordTypeName, null);
} else {
throw new RecordException("The record '" + record + "' should specify a record type");
}
// Get and sort the field type entries that should be in the record
List<FieldType> fieldTypes = getSortedFieldTypes(recordType);
Map<QName, Object> recordFields = record.getFields();
List<QName> expectedFields = new ArrayList<QName>();
// Write the record type information
// Encoding:
// - encoding version : byte (1)
// - nr of bytes in recordtype id : vInt
// - recordtype id : bytes
// - recordtype version : long
dataOutput.writeByte(ENCODING_VERSION);
byte[] recordIdBytes = recordType.getId().getBytes();
dataOutput.writeVInt(recordIdBytes.length);
dataOutput.writeBytes(recordIdBytes);
dataOutput.writeLong(recordType.getVersion());
// Write the content of the fields
// Encoding: for each field :
// - if not present in the record : undefined marker : byte (0)
// - if present in the record : defined marker : byte (1)
// - fieldValue : bytes
for (FieldType fieldType : fieldTypes) {
QName name = fieldType.getName();
expectedFields.add(name);
Object fieldValue = recordFields.get(name);
if (fieldValue == null) {
dataOutput.writeByte(UNDEFINED);
} else {
dataOutput.writeByte(DEFINED);
parentRecords.push(record);
fieldType.getValueType().write(fieldValue, dataOutput, parentRecords);
parentRecords.pop();
}
}
// Check if the record does contain fields that are not defined in the record type
if (!expectedFields.containsAll(recordFields.keySet())) {
throw new InvalidRecordException("Record contains fields not part of the record type '" +
recordType.getName() + "'", record.getId());
}
}
private List<FieldType> getSortedFieldTypes(RecordType recordType) throws RepositoryException,
InterruptedException {
Collection<FieldTypeEntry> fieldTypeEntries = getFieldTypeEntries(recordType);
List<FieldType> fieldTypes = new ArrayList<FieldType>();
for (FieldTypeEntry fieldTypeEntry : fieldTypeEntries) {
fieldTypes.add(typeManager.getFieldTypeById(fieldTypeEntry.getFieldTypeId()));
}
return fieldTypes;
}
private Collection<FieldTypeEntry> getFieldTypeEntries(RecordType recordType)
throws RepositoryException, InterruptedException {
// Wrap the list as an array list since we don't know if the collection will actually support the .addAll() methodq
Collection<FieldTypeEntry> fieldTypeEntries = new ArrayList<FieldTypeEntry>(recordType.getFieldTypeEntries());
Map<SchemaId, Long> supertypes = recordType.getSupertypes();
for (Entry<SchemaId, Long> supertypeEntry: supertypes.entrySet()) {
RecordType supertypeRecordType = typeManager.getRecordTypeById(supertypeEntry.getKey(), supertypeEntry.getValue());
fieldTypeEntries.addAll(getFieldTypeEntries(supertypeRecordType));
}
return fieldTypeEntries;
}
@Override
public Class getType() {
return Record.class;
}
@Override
public Comparator getComparator() {
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + fullName.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;
}
return fullName.equals(((RecordValueType) obj).fullName);
}
//
// Factory
//
public static ValueTypeFactory factory(TypeManager typeManager) {
return new RecordValueTypeFactory(typeManager);
}
public static class RecordValueTypeFactory implements ValueTypeFactory {
private TypeManager typeManager;
public RecordValueTypeFactory(TypeManager typeManager) {
this.typeManager = typeManager;
}
@Override
public ValueType getValueType(String recordName) throws IllegalArgumentException, RepositoryException,
InterruptedException {
return new RecordValueType(typeManager, recordName);
}
}
}