/*
* 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.api;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import org.lilyproject.bytes.api.DataInput;
import org.lilyproject.bytes.api.DataOutput;
import org.lilyproject.util.ArgumentValidator;
import org.lilyproject.util.ObjectUtils;
/**
* A link to another record.
*
* <p>The difference between a Link and a RecordId is that a Link can be relative. This means
* that the link needs to be resolved against the context it occurs in (= the record it occurs in)
* in order to get the actual id of the record it points to.
*
* <h2>About relative links</h2>
*
* <p>As an example, suppose we have a record with a variant property 'language=en'. In this
* record, we want to link to another record, also having the 'language=en'. By using Link,
* we can specify only the master record ID of the target record, without the 'language=en'
* property. The 'language=en' will be inherited from the record in which the link occurs.
* The advantage is that if the link is copied to another record with e.g. 'language=fr',
* the link will automatically adjust to this context.
*
* <p>Links have the following possibilities to specify the relative link:
*
* <ul>
* <li>the master record id itself is optional, and can be copied (= inherited) from the context.
* <li>it is possible to specify that all variant properties from the context should be copied.
* This is specified by the "copyAll" property of this object.
* <li>it is possible to specify individual properties. For each individual property, you can
* specify one of the following:
* <ul>
* <li>an exact value
* <li>copy the value from the context (if any). This allows to selectively copy some variant
* properties from the context. This only makes sense when not using copyAll.
* <li>remove the value, if it would have been copied from the context. This is useful if you
* want to copy all variant properties from the context (not knowing how many there are),
* except some of them.
* </ul>
* </ul>
*
* <h2>Creating links</h2>
*
* <p>If you just want a link to point to an exact record id, use the constructor
* <tt>new Link(recordId, false)</tt>.
*
* <p>To have a link that copies variant properties from the context, use:
* <tt>new Link(recordId)</tt>.
*
* <p>In such cases you might want to consider creating the link based only on the master record id
*
* <tt>new Link(recordId.getMaster())</tt>.
*
* <p>The Link class is immutable after construction. You have to either pass all properties
* through constructor arguments, or use the LinkBuilder class obtained via {@link #newBuilder}.
*
* <p>Example using LinkBuilder:
*
* <pre>
* RecordId recordId = ...;
* Link link = Link.newBuilder().recordId(recordId).copyAll(false).copy("dev").set("foo", "bar").create();
* </pre>
*
* <h2>Resolving links to RecordId's</h2>
*
* <p>To resolve a link to a RecordId, using the {@link #resolve(RecordId, IdGenerator)} method.
*/
public class Link {
private String table;
private RecordId masterRecordId;
private boolean copyAll = true;
private SortedMap<String, PropertyValue> variantProps;
/**
* A link to self.
*/
public Link() {
}
/**
* If copyAll is true, a link to self, if copy all is false, a link to the master.
*/
public Link(boolean copyAll) {
this.copyAll = copyAll;
}
/**
* An absolute link to the specified recordId. Nothing will be copied from the context
* when resolving this link.
*/
public Link(RecordId recordId) {
this((String)null, recordId);
}
/**
* An absolute link to the specified recordId. Nothing will be copied from the context
* when resolving this link, also with a repository table name supplied.
*/
public Link(String table, RecordId recordId) {
this.table = table;
this.masterRecordId = recordId != null ? recordId.getMaster() : null;
variantProps = createVariantProps(recordId);
}
/**
* A relative link to the specified recordId. All variant properties will be copied
* from the context when resolving this link, except those that would be explicitly
* specified on the recordId.
*/
public Link(RecordId recordId, boolean copyAll) {
this((String)null, recordId, copyAll);
}
/**
* A relative link to the specified recordId. All variant properties will be copied
* from the context when resolving this link, except those that would be explicitly
* specified on the recordId. A table name is also supplied.
*/
public Link(String tableName, RecordId recordId, boolean copyAll) {
this(tableName, recordId);
this.copyAll = copyAll;
}
private Link(String table, RecordId masterRecordId, boolean copyAll, SortedMap<String, PropertyValue> props) {
this.table = table;
this.masterRecordId = masterRecordId;
this.copyAll = copyAll;
this.variantProps = props;
}
private static SortedMap<String, PropertyValue> createVariantProps(RecordId recordId) {
if (recordId == null) {
return null;
}
SortedMap<String, PropertyValue> variantProps = null;
if (!recordId.isMaster()) {
variantProps = new TreeMap<String, PropertyValue>();
for (Map.Entry<String, String> entry : recordId.getVariantProperties().entrySet()) {
variantProps.put(entry.getKey(), new PropertyValue(PropertyMode.SET, entry.getValue()));
}
}
return variantProps;
}
/** FIXME: copy-paste job from IdGeneratorImpl, add to utility class? */
private static String[] escapedSplit(String s, char delimiter, int maxSplits) {
ArrayList<String> split = new ArrayList<String>();
StringBuffer sb = new StringBuffer();
boolean escaped = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (escaped) {
escaped = false;
sb.append(c);
} else if (delimiter == c) {
if (maxSplits == -1 || split.size() < maxSplits) {
split.add(sb.toString());
sb = new StringBuffer();
} else {
sb.append(c);
}
} else if ('\\' == c) {
escaped = true;
sb.append(c);
} else {
sb.append(c);
}
}
split.add(sb.toString());
return split.toArray(new String[0]);
}
/**
* Parses a link in the syntax produced by {@link #toString()}.
*
* <p>An empty string is interpreted as a link-to-self, thus the same as ".".
*
* <p>If the same variant property would be specified multiple times, it is its
* last occurrence which will count.
*
* @throws IllegalArgumentException in case of syntax errors in the link.
*/
public static Link fromString(String link, IdGenerator idGenerator) {
ArgumentValidator.notNull(link, "link");
// link to self
if (link.equals("") || link.equals(".")) {
return new Link();
}
String table = null;
RecordId recordId;
String variantString;
if (link.startsWith(".")) {
recordId = null;
variantString = link.substring(1);
} else {
int firstDotPos = link.indexOf('.');
int firstColonPos = link.indexOf(':');
if (firstDotPos == -1) {
throw new IllegalArgumentException("Invalid link, contains no dot: " + link);
}
if (firstColonPos != -1 && firstColonPos < firstDotPos) {
String[] tableSplitParts = escapedSplit(link, ':', 1);
table = tableSplitParts[0];
link = tableSplitParts[1];
}
String[] parts = escapedSplit(link, '.', -1);
String masterIdString = parts[0] + "." + parts[1];
if (parts.length < 3) {
variantString = null;
} else {
variantString = parts[2];
}
recordId = idGenerator.fromString(masterIdString);
}
if (variantString == null) {
return new Link(table, recordId, true);
}
LinkBuilder builder = Link.newBuilder().recordId(recordId).table(table);
argsFromString(variantString, builder, link);
return builder.create();
}
private static void argsFromString(String args, LinkBuilder builder, String link) {
String[] variantStringParts = args.split(",");
for (String part : variantStringParts) {
int eqPos = part.indexOf('=');
if (eqPos == -1) {
String thing = part.trim();
if (thing.equals("*")) {
// this is the default, but if users want to make explicit, allow them
builder.copyAll(true);
} else if (thing.equals("!*")) {
builder.copyAll(false);
} else if (thing.startsWith("+") && thing.length() > 1) {
builder.copy(thing.substring(1));
} else if (thing.startsWith("-") && thing.length() > 1) {
builder.remove(thing.substring(1));
} else {
throw new IllegalArgumentException("Invalid link: " + link);
}
} else {
String name = part.substring(0, eqPos).trim();
String value = part.substring(eqPos + 1).trim();
if (name.length() == 0 || value.length() == 0) {
throw new IllegalArgumentException("Invalid link: " + link);
}
builder.set(name, value);
}
}
}
/**
* Returns the (optional) repository table name for resolving this link.
*
* @return the repository table name, or null if no repository table name has been supplied
*/
public String getTable() {
return table;
}
public RecordId getMasterRecordId() {
return masterRecordId;
}
public boolean copyAll() {
return copyAll;
}
public Map<String, PropertyValue> getVariantProps() {
return Collections.unmodifiableMap(variantProps);
}
/**
* Creates a string representation of this link.
*
* <p>The syntax is:
*
* <pre>{recordId}.!*,arg1=val1,+arg2,-arg3<pre>
*
* <p>The recordId is optional. Arguments, if any, follow after the . symbol and are separated by ','
* symbols. Note that the {recordId} itself also contains a dot to separate the record id type and its
* actual content (e.g. USER.235523432).
*
* <p>The '!*' argument indicates 'not copyAll', copyAll is the default if not specified.
*
* <p><tt>arg1=val1</tt> is an example of specifying an exact value for a variant property.
*
* <p><tt>+arg2</tt> is an explicit copy of the variant property 'arg2' from the context. Does
* only make sense when not using copyAll, thus when !* is in the link.
*
* <p><<tt>-arg3</tt> is a removal (exclusion) of a variant property copied by using copyAll.
*
* <p>The arguments will always be specified in alphabetical order, ignoring the + or - symbol.
* "Not copyAll" (!*) is always the first argument.
*/
@Override
public String toString() {
if (masterRecordId == null && variantProps == null) {
if (copyAll) {
// link to self
return ".";
} else {
// link to my master
return ".!*";
}
}
StringBuilder builder = new StringBuilder();
if (table != null) {
builder.append(table + ":");
}
if (masterRecordId != null) {
builder.append(masterRecordId.toString());
}
if (!copyAll || variantProps != null) {
builder.append(".");
argstoString(builder);
}
return builder.toString();
}
private void argstoString(StringBuilder builder) {
boolean firstArg = true;
if (!copyAll) {
builder.append("!*");
firstArg = false;
}
if (variantProps != null) {
for (Map.Entry<String, PropertyValue> entry : variantProps.entrySet()) {
if (firstArg) {
firstArg = false;
} else {
builder.append(",");
}
switch (entry.getValue().mode) {
case COPY:
builder.append('+').append(entry.getKey());
break;
case REMOVE:
builder.append('-').append(entry.getKey());
break;
case SET:
builder.append(entry.getKey()).append('=').append(entry.getValue().value);
break;
}
}
}
}
public void write(DataOutput dataOutput) {
// The bytes format is as follows:
// [byte representation of table, byte representation of master record id, if not null][args: bytes of the string representation]
byte[] tableBytes = table == null ? new byte[0] : table.getBytes();
dataOutput.writeInt(tableBytes.length);
if (tableBytes.length > 0) {
dataOutput.writeBytes(tableBytes);
}
byte[] recordIdBytes = masterRecordId == null ? new byte[0] : masterRecordId.toBytes();
dataOutput.writeInt(recordIdBytes.length);
if (recordIdBytes.length > 0) {
dataOutput.writeBytes(recordIdBytes);
}
StringBuilder argsBuilder = new StringBuilder();
argstoString(argsBuilder);
dataOutput.writeUTF(argsBuilder.toString());
}
public static Link read(DataInput dataInput, IdGenerator idGenerator) {
// Format: see write(DataOutput).
int tableLength = dataInput.readInt();
byte[] tableBytes = dataInput.readBytes(tableLength);
int recordIdLength = dataInput.readInt();
byte[] recordIdBytes = null;
if (recordIdLength > 0) {
recordIdBytes = dataInput.readBytes(recordIdLength);
}
String args = dataInput.readUTF();
if (recordIdLength == 0 && args == null) {
return new Link();
}
LinkBuilder builder = Link.newBuilder();
if (tableLength > 0) {
builder.table(new String(tableBytes));
}
if (recordIdLength > 0) {
RecordId id = idGenerator.fromBytes(recordIdBytes);
builder.recordId(id);
}
if (args != null && args.length() > 0) {
argsFromString(args, builder, args /* does not matter, should never be invalid */);
}
return builder.create();
}
/**
* A shortcut for resolve(contextRecord.getId(), idGenerator).
*/
public RecordId resolve(Record contextRecord, IdGenerator idGenerator) {
return resolve(contextRecord.getId(), idGenerator);
}
/**
* Resolves this link to a concrete, absolute RecordId.
*
* @param contextRecordId usually the id of the record in which this link occurs.
*/
public RecordId resolve(RecordId contextRecordId, IdGenerator idGenerator) {
RecordId masterRecordId = this.masterRecordId == null ? contextRecordId.getMaster() : this.masterRecordId;
Map<String, String> varProps = null;
// the if statement is just an optimisation to avoid the map creation if not necessary
if (variantProps != null ||
(this.masterRecordId != null && !this.masterRecordId.isMaster()) ||
(copyAll && !contextRecordId.isMaster())) {
varProps = new HashMap<String, String>();
// Optionally copy over the properties from the context record id
if (copyAll) {
for (Map.Entry<String, String> entry : contextRecordId.getVariantProperties().entrySet()) {
if (!varProps.containsKey(entry.getKey())) {
varProps.put(entry.getKey(), entry.getValue());
}
}
}
// Process the manual specified variant properties
if (variantProps != null) {
evalProps(varProps, contextRecordId);
}
}
if (varProps == null || varProps.isEmpty()) {
return masterRecordId;
} else {
return idGenerator.newRecordId(masterRecordId, varProps);
}
}
public enum PropertyMode {SET, COPY, REMOVE}
public static class PropertyValue {
private PropertyMode mode;
private String value;
private PropertyValue(PropertyMode mode, String value) {
ArgumentValidator.notNull(mode, "mode");
this.mode = mode;
if (mode == PropertyMode.SET) {
ArgumentValidator.notNull(value, "value");
this.value = value;
}
}
public PropertyMode getMode() {
return mode;
}
/**
* Value is only defined when the PropertyMode is SET.
*/
public String getValue() {
return value;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final PropertyValue that = (PropertyValue) o;
if (mode != that.mode) {
return false;
}
if (!ObjectUtils.safeEquals(value, that.value)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = mode != null ? mode.hashCode() : 0;
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
}
private void evalProps(Map<String, String> resolvedProps, RecordId contextRecordId) {
Map<String, String> contextProps = contextRecordId.getVariantProperties();
for (Map.Entry<String, PropertyValue> entry : variantProps.entrySet()) {
PropertyValue propValue = entry.getValue();
switch (propValue.mode) {
case SET:
resolvedProps.put(entry.getKey(), propValue.value);
break;
case REMOVE:
resolvedProps.remove(entry.getKey());
break;
case COPY:
String value = contextProps.get(entry.getKey());
if (value != null) {
resolvedProps.put(entry.getKey(), value);
}
break;
}
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Link other = (Link)obj;
if (!ObjectUtils.safeEquals(masterRecordId, other.masterRecordId)) {
return false;
}
if (copyAll != other.copyAll) {
return false;
}
if (variantProps == null && other.variantProps == null) {
return true;
}
if ((variantProps == null && other.variantProps != null) || (variantProps != null && other.variantProps == null)) {
return false;
}
if (variantProps.size() != other.variantProps.size()) {
return false;
}
for (Map.Entry<String, PropertyValue> entry : variantProps.entrySet()) {
PropertyValue otherVal = other.variantProps.get(entry.getKey());
if (otherVal == null) {
return false;
}
PropertyValue val = entry.getValue();
if (val.mode != otherVal.mode) {
return false;
}
if (!ObjectUtils.safeEquals(val.value, otherVal.value)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int result = masterRecordId != null ? masterRecordId.hashCode() : 0;
result = 31 * result + (copyAll ? 1 : 0);
result = 31 * result + (variantProps != null ? variantProps.hashCode() : 0);
return result;
}
public static LinkBuilder newBuilder() {
return new LinkBuilder();
}
public static class LinkBuilder {
private String table;
private RecordId masterRecordId;
private boolean copyAll = true;
private Map<String, PropertyValue> variantProps;
private LinkBuilder() {
}
/**
* Calling this resets the state of the variant properties recorded so far.
*/
public LinkBuilder recordId(RecordId recordId) {
if (recordId != null) {
this.masterRecordId = recordId.getMaster();
this.variantProps = createVariantProps(recordId);
} else {
this.masterRecordId = null;
this.variantProps = null;
}
return this;
}
public LinkBuilder table(String table) {
this.table = table;
return this;
}
public LinkBuilder copyAll(boolean copyAll) {
this.copyAll = copyAll;
return this;
}
public LinkBuilder copy(String propName) {
ArgumentValidator.notNull(propName, "propName");
initVarProps();
variantProps.put(propName, new PropertyValue(PropertyMode.COPY, null));
return this;
}
public LinkBuilder remove(String propName) {
ArgumentValidator.notNull(propName, "propName");
initVarProps();
variantProps.put(propName, new PropertyValue(PropertyMode.REMOVE, null));
return this;
}
public LinkBuilder set(String propName, String propValue) {
ArgumentValidator.notNull(propName, "propName");
ArgumentValidator.notNull(propValue, "propValue");
initVarProps();
variantProps.put(propName, new PropertyValue(PropertyMode.SET, propValue));
return this;
}
public Link create() {
if (variantProps == null) {
return new Link(table, masterRecordId, copyAll);
} else {
return new Link(table, masterRecordId, copyAll, new TreeMap<String, PropertyValue>(variantProps));
}
}
private void initVarProps() {
if (variantProps == null) {
variantProps = new HashMap<String, PropertyValue>();
}
}
}
}