blob: 75b6f92e99c4ec3e21eef58362667b18ef5296b3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.jackrabbit.vault.util;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import org.apache.jackrabbit.api.ReferenceBinary;
import org.apache.jackrabbit.commons.jackrabbit.SimpleReferenceBinary;
import org.apache.jackrabbit.util.XMLChar;
import org.apache.jackrabbit.value.ValueHelper;
/**
* Helper class that represents a (jcr) property in the document view format.
* It contains formatting and parsing methods for writing/reading enhanced
* docview properties.
*
* {@code prop:= [ "{" type "}" ] ( value | "[" [ value { "," value } ] "]" )}
*/
public class DocViewProperty {
private static final String BINARY_REF = "BinaryRef";
/**
* name of the property
*/
public final String name;
/**
* value(s) of the property. always contains at least one value if this is
* not a mv property.
*/
public final String[] values;
/**
* indicates a MV property
*/
public final boolean isMulti;
/**
* type of this property (can be undefined)
*/
public final int type;
/**
* indicates a binary ref property
*/
public final boolean isReferenceProperty;
/**
* set of unambigous property names
*/
private static final Set<String> UNAMBIGOUS = new HashSet<String>();
static {
UNAMBIGOUS.add("jcr:primaryType");
UNAMBIGOUS.add("jcr:mixinTypes");
}
/**
* Creates a new property.
* @param name name of the property
* @param values values.
* @param multi multiple flag
* @param type type of the property
* @throws IllegalArgumentException if single value property and not exactly 1 value is given.
*/
public DocViewProperty(String name, String[] values, boolean multi, int type) {
this(name, values, multi, type, false);
}
/**
* Creates a new property.
* @param name name of the property
* @param values values.
* @param multi multiple flag
* @param type type of the property
* @param isRef {@code true} to indicated that this is a binary reference property
* @throws IllegalArgumentException if single value property and not exactly 1 value is given.
*/
public DocViewProperty(String name, String[] values, boolean multi, int type, boolean isRef) {
this.name = name;
this.values = values;
isMulti = multi;
// validate type
if (type == PropertyType.UNDEFINED) {
if ("jcr:primaryType".equals(name) || "jcr:mixinTypes".equals(name)) {
type = PropertyType.NAME;
}
}
this.type = type;
if (!isMulti && values.length != 1) {
throw new IllegalArgumentException("Single value property needs exactly 1 value.");
}
this.isReferenceProperty = isRef;
}
/**
* Parses a enhanced docview property string and returns the property.
* @param name name of the property
* @param value (attribute) value
* @return a property
*/
public static DocViewProperty parse(String name, String value) {
boolean isMulti = false;
boolean isBinaryRef = false;
int type = PropertyType.UNDEFINED;
int pos = 0;
char state = 'b';
List<String> vals = null;
StringBuffer tmp = new StringBuffer();
int unicode = 0;
int unicodePos = 0;
while (pos < value.length()) {
char c = value.charAt(pos++);
switch (state) {
case 'b': // begin (type or array or value)
if (c == '{') {
state = 't';
} else if (c == '[') {
isMulti = true;
state = 'v';
} else if (c == '\\') {
state = 'e';
} else {
tmp.append(c);
state = 'v';
}
break;
case 'a': // array (array or value)
if (c == '[') {
isMulti = true;
state = 'v';
} else if (c == '\\') {
state = 'e';
} else {
tmp.append(c);
state = 'v';
}
break;
case 't':
if (c == '}') {
if (BINARY_REF.equals(tmp.toString())) {
type = PropertyType.BINARY;
isBinaryRef = true;
} else {
type = PropertyType.valueFromName(tmp.toString());
}
tmp.setLength(0);
state = 'a';
} else {
tmp.append(c);
}
break;
case 'v': // value
if (c == '\\') {
state = 'e';
} else if (c == ',' && isMulti) {
if (vals == null) {
vals = new LinkedList<String>();
}
vals.add(tmp.toString());
tmp.setLength(0);
} else if (c == ']' && isMulti && pos == value.length()) {
if (tmp.length() > 0 || vals != null) {
if (vals == null) {
vals = new LinkedList<String>();
}
vals.add(tmp.toString());
tmp.setLength(0);
}
} else {
tmp.append(c);
}
break;
case 'e': // escaped
if (c == 'u') {
state = 'u';
unicode = 0;
unicodePos = 0;
} else if (c == '0') {
// special case to treat empty values. see JCR-3661
state = 'v';
if (vals == null) {
vals = new LinkedList<String>();
}
} else {
state = 'v';
tmp.append(c);
}
break;
case 'u': // unicode escaped
unicode = (unicode << 4) + Character.digit(c, 16);
if (++unicodePos == 4) {
tmp.appendCodePoint(unicode);
state = 'v';
}
break;
}
}
String[] values;
if (isMulti) {
// add value if missing ']'
if (tmp.length() > 0) {
if (vals == null) {
vals = new LinkedList<String>();
}
vals.add(tmp.toString());
}
if (vals == null) {
values = Constants.EMPTY_STRING_ARRAY;
} else {
values = vals.toArray(new String[vals.size()]);
}
} else {
values = new String[]{tmp.toString()};
}
return new DocViewProperty(name, values, isMulti, type, isBinaryRef);
}
/**
* Formats the given jcr property to the enhanced docview syntax.
* @param prop the jcr property
* @return the formatted string
* @throws RepositoryException if a repository error occurs
*/
public static String format(Property prop) throws RepositoryException {
return format(prop, false, false);
}
/**
* Formats the given jcr property to the enhanced docview syntax.
* @param prop the jcr property
* @param sort if {@code true} multivalue properties are sorted
* @param useBinaryReferences {@code true} to use binary references
* @return the formatted string
* @throws RepositoryException if a repository error occurs
*/
public static String format(Property prop, boolean sort, boolean useBinaryReferences)
throws RepositoryException {
StringBuffer attrValue = new StringBuffer();
int type = prop.getType();
if (type == PropertyType.BINARY || isAmbiguous(prop)) {
String referenceBinary = null;
if (useBinaryReferences && type == PropertyType.BINARY) {
Binary bin = prop.getBinary();
if (bin != null && bin instanceof ReferenceBinary) {
referenceBinary = ((ReferenceBinary) bin).getReference();
}
}
if (referenceBinary == null) {
attrValue.append("{");
attrValue.append(PropertyType.nameFromValue(prop.getType()));
attrValue.append("}");
} else {
attrValue.append("{");
attrValue.append(BINARY_REF);
attrValue.append("}");
attrValue.append(referenceBinary);
}
}
// only write values for non binaries
if (prop.getType() != PropertyType.BINARY) {
if (prop.getDefinition().isMultiple()) {
attrValue.append('[');
Value[] values = prop.getValues();
if (sort) {
Arrays.sort(values, ValueComparator.getInstance());
}
for (int i = 0; i < values.length; i++) {
if (i > 0) {
attrValue.append(',');
}
String strValue = ValueHelper.serialize(values[i], false);
if (values.length == 1 && strValue.length() == 0) {
// special case for empty string MV value (JCR-3661)
attrValue.append("\\0");
} else {
switch (prop.getType()) {
case PropertyType.STRING:
case PropertyType.NAME:
case PropertyType.PATH:
escape(attrValue, strValue, true);
break;
default:
attrValue.append(strValue);
}
}
}
attrValue.append(']');
} else {
String strValue = ValueHelper.serialize(prop.getValue(), false);
escape(attrValue, strValue, false);
}
}
return attrValue.toString();
}
/**
* Escapes the value
* @param buf buffer to append to
* @param value value to escape
* @param isMulti indicates multi value property
*/
protected static void escape(StringBuffer buf, String value, boolean isMulti) {
for (int i=0; i<value.length(); i++) {
char c = value.charAt(i);
if (c == '\\') {
buf.append("\\\\");
} else if (c == ',' && isMulti) {
buf.append("\\,");
} else if (i == 0 && !isMulti && (c == '[' || c == '{')) {
buf.append('\\').append(c);
} else if ( XMLChar.isInvalid(c)) {
buf.append("\\u");
buf.append(Text.hexTable[(c >> 12) & 15]);
buf.append(Text.hexTable[(c >> 8) & 15]);
buf.append(Text.hexTable[(c >> 4) & 15]);
buf.append(Text.hexTable[c & 15]);
} else {
buf.append(c);
}
}
}
/**
* Checks if the type of the given property is ambiguous in respect to it's
* property definition. the current implementation just checks some well
* known properties.
*
* @param prop the property
* @return type
* @throws RepositoryException if a repository error occurs
*/
public static boolean isAmbiguous(Property prop) throws RepositoryException {
return prop.getType() != PropertyType.STRING && !UNAMBIGOUS.contains(prop.getName());
}
/**
* Sets this property on the given node
*
* @param node the node
* @return {@code true} if the value was modified.
* @throws RepositoryException if a repository error occurs
*/
public boolean apply(Node node) throws RepositoryException {
Property prop = node.hasProperty(name) ? node.getProperty(name) : null;
// check if multiple flags are equal
if (prop != null && isMulti != prop.getDefinition().isMultiple()) {
prop.remove();
prop = null;
}
if (prop != null) {
int propType = prop.getType();
if (propType != type && (propType != PropertyType.STRING || type != PropertyType.UNDEFINED)) {
// never compare if types differ
prop = null;
}
}
if (isMulti) {
// todo: handle multivalue binaries and reference binaries
Value[] vs = prop == null ? null : prop.getValues();
if (vs != null && vs.length == values.length) {
// quick check all values
boolean modified = false;
for (int i=0; i<vs.length; i++) {
if (!vs[i].getString().equals(values[i])) {
modified = true;
}
}
if (!modified) {
return false;
}
}
if (type == PropertyType.UNDEFINED) {
node.setProperty(name, values);
} else {
node.setProperty(name, values, type);
}
// assume modified
return true;
} else {
Value v = prop == null ? null : prop.getValue();
if (type == PropertyType.BINARY) {
if (isReferenceProperty) {
ReferenceBinary ref = new SimpleReferenceBinary(values[0]);
Binary binary = node.getSession().getValueFactory().createValue(ref).getBinary();
if (v != null) {
Binary bin = v.getBinary();
if (bin.equals(binary)) {
return false;
}
}
node.setProperty(name, binary);
}
// the binary property is always modified (TODO: check if still correct with JCRVLT-110)
return true;
}
if (v == null || !v.getString().equals(values[0])) {
try {
if (type == PropertyType.UNDEFINED) {
node.setProperty(name, values[0]);
} else {
node.setProperty(name, values[0], type);
}
} catch (ValueFormatException e) {
// forcing string
node.setProperty(name, values[0], PropertyType.STRING);
}
return true;
}
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (isMulti ? 1231 : 1237);
result = prime * result + (isReferenceProperty ? 1231 : 1237);
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + type;
result = prime * result + Arrays.hashCode(values);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DocViewProperty other = (DocViewProperty) obj;
if (isMulti != other.isMulti)
return false;
if (isReferenceProperty != other.isReferenceProperty)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (type != other.type)
return false;
if (!Arrays.equals(values, other.values))
return false;
return true;
}
@Override
public String toString() {
return "DocViewProperty [name=" + name + ", values=" + Arrays.toString(values) + ", isMulti=" + isMulti + ", type=" + type
+ ", isReferenceProperty=" + isReferenceProperty + "]";
}
}