package org.odata4j.core;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.core4j.Enumerable;
import org.core4j.Func1;
import org.odata4j.edm.EdmEntitySet;
import org.odata4j.edm.EdmEntityType;
import org.odata4j.edm.EdmProperty;
import org.odata4j.edm.EdmSimpleType;
import org.odata4j.expression.CommonExpression;
import org.odata4j.expression.Expression;
import org.odata4j.expression.ExpressionParser;
import org.odata4j.expression.LiteralExpression;
/**
* An immutable entity-key, made up of either a single unnamed-value or multiple named-values.
* Every entity must have an entity-key. The entity-key must be unique within the entity-set, and thus defines an entity's identity. (see {@link OEntityId})
* <p>An entity-key made up a a single unnamed-value is called a single key. An entity-key made up of multiple named-values is called a complex key.</p>
* <p>The string representation of an entity-key is wrapped with parentheses, such as <code>(2)</code>, <code>('foo')</code> or <code>(a=1,foo='bar')</code>.</p>
* <p>Entity-keys are equal if their string representations are equal.</p>
*/
public class OEntityKey {
/**
* SINGLE or COMPLEX
*/
public enum KeyType {
SINGLE,
COMPLEX
}
private final Object[] values;
private final String keyString;
private OEntityKey(Object[] values) {
this.values = values;
this.keyString = keyString(values);
}
/**
* Creates an entity-key.
* <ul>
* <li><code>OEntityKey.create(2)</code></li>,
* <li><code>OEntityKey.create("foo")</code></li>
* <li><code>OEntityKey.create("a",1,"foo","bar")</code></li>
* <li><code>OEntityKey.create(NamedValues.create("a",1),NamedValues.create("foo","bar"))</code></li>
* </ul></p>
*
* @param values the key values
* @return a newly-created entity-key
*/
@SuppressWarnings("unchecked")
public static OEntityKey create(Object... values) {
if (values != null && values.length == 1 && values[0] instanceof Iterable<?>)
return create(Enumerable.create((Iterable<Object>) values[0]).toArray(Object.class));
if (values != null && values.length == 1 && values[0] instanceof OEntityKey)
return (OEntityKey) values[0];
if (values != null && values.length == 1 && values[0] instanceof OEntityId)
return ((OEntityId) values[0]).getEntityKey();
if (values != null && values.length > 1 && values.length % 2 == 0 && values[0] instanceof String) {
Map<String, Object> rt = new HashMap<String, Object>();
for (int i = 0; i < values.length; i += 2) {
String name = (String) values[i];
Object value = values[i + 1];
rt.put(name, value);
}
return create(rt);
}
Object[] v = validate(values);
return new OEntityKey(v);
}
/**
* Creates an entity-key from a map of names and values.
*
* @param values the map of names and values
* @return a newly-created entity-key
*/
public static OEntityKey create(Map<String, Object> values) {
return create(NamedValues.fromMap(values));
}
/**
* Creates an entity-key using key information from the given entity-set and values from the given property list.
*
* @param entitySet an entity-set to provide key information
* @param props a list of properties to provide key values
* @return a newly-created entity-key
*/
public static OEntityKey infer(EdmEntitySet entitySet, List<OProperty<?>> props) {
if (entitySet == null)
throw new IllegalArgumentException("EdmEntitySet cannot be null");
if (props == null)
throw new IllegalArgumentException("props cannot be null");
EdmEntityType eet = entitySet.getType();
if (eet == null)
throw new IllegalArgumentException("EdmEntityType cannot be null");
List<String> keys = eet.getKeys();
if (keys.size() == 0) {
String idProp = Enumerable.create(eet.getProperties())
.select(OFuncs.name(EdmProperty.class))
.firstOrNull(OPredicates.equalsIgnoreCase("id"));
if (idProp != null)
keys.add(idProp);
}
Object[] v = new Object[keys.size()];
for (int i = 0; i < v.length; i++) {
String keyPropertyName = keys.get(i);
v[i] = getProp(props, keyPropertyName);
}
v = validate(v);
return new OEntityKey(v);
}
/**
* Creates an entity-key from its standard string representation.
*
* @param keyString a standard key-string
* @return a newly-created entity-key
*/
public static OEntityKey parse(String keyString) {
if (keyString == null)
throw new IllegalArgumentException("keyString cannot be null");
keyString = keyString.trim();
if (keyString.startsWith("(") && keyString.endsWith(")"))
keyString = keyString.substring(1, keyString.length() - 1);
keyString = keyString.trim();
if (keyString.length() == 0)
throw new IllegalArgumentException("keyString cannot be blank");
String[] tokens = tokens(keyString, ',');
List<Object> values = new ArrayList<Object>(tokens.length);
for (String token : tokens) {
String[] nv = tokens(token, '=');
if (nv.length != 1 && nv.length != 2)
throw new IllegalArgumentException("bad keyString: " + keyString);
String valueString = nv.length == 1 ? nv[0] : nv[1];
try {
// the key might be url encoded if it comes from delete request etc as part DELETE request, decode it here.
// Parser will do decoding if there is entity in POST payload.
//valueString = ConversionUtil.decodeString(valueString);
CommonExpression expr = ExpressionParser.parse(valueString);
LiteralExpression literal = (LiteralExpression) expr;
Object value = Expression.literalValue(literal);
values.add(nv.length == 1 ? value : NamedValues.create(nv[0], value));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("bad valueString [%s] as part of keyString [%s]", valueString, keyString), e);
}
}
return OEntityKey.create(values);
}
private static String[] tokens(String ks, char sep) {
List<String> rt = new ArrayList<String>();
boolean inString = false;
int start = 0;
for (int i = 0; i < ks.length(); i++) {
char c = ks.charAt(i);
if (c == sep && !inString) {
rt.add(ks.substring(start, i));
start = i + 1;
continue;
}
if (c == '\'') {
if (!inString) {
inString = true;
continue;
}
if (i < ks.length() - 1) {
char next = ks.charAt(i + 1);
if (next == '\'') {
i++;
continue;
}
}
inString = false;
}
}
rt.add(ks.substring(start, ks.length()));
return rt.toArray(new String[rt.size()]);
}
@Override
public String toString() {
return toKeyString();
}
/**
* Gets the standard string representation of this entity-key, including parentheses.
*
* @return the standard key-string
*/
public String toKeyString() {
return keyString;
}
/**
* Gets the standard string representation of this entity-key, excluding parentheses.
*
* @return the standard key-string, without parentheses
*/
public String toKeyStringWithoutParentheses() {
String keyString = this.keyString;
if (keyString.startsWith("(") && keyString.endsWith(")"))
keyString = keyString.substring(1, keyString.length() - 1);
return keyString;
}
@Override
public int hashCode() {
return keyString.hashCode();
}
@Override
public boolean equals(Object obj) {
return (obj instanceof OEntityKey)
&& ((OEntityKey) obj).keyString.equals(keyString);
}
/**
* Gets a the value of a single-valued entity-key.
*
* @return the key value
*/
public Object asSingleValue() {
if (values.length > 1)
throw new RuntimeException("Complex key cannot be represented as a single value");
return values[0];
}
/**
* Gets the values of a complex entity-key.
*
* @return the key values as a set of named-values
*/
@SuppressWarnings("unchecked")
public Set<NamedValue<?>> asComplexValue() {
assertComplex();
return (Set<NamedValue<?>>) (Object) toSortedSet(Enumerable.create(values).cast(NamedValue.class), OComparators.namedValueByNameRaw());
}
/**
* Gets the values of complex entity-key.
*
* @return the key values as a set of properties
*/
public Set<OProperty<?>> asComplexProperties() {
assertComplex();
return toSortedSet(Enumerable.create(values).cast(NamedValue.class).select(OFuncs.namedValueToPropertyRaw()), OComparators.propertyByName());
}
/**
* returns the value for a named value in a complex key.
* @param name
* @return the value
*/
public Object getComplexKeyValue(String name) {
assertComplex();
for (Object o : this.values) {
NamedValue<?> nv = (NamedValue<?>) o;
if (nv.getName().equals(name)) {
return nv.getValue();
}
}
return null;
}
private static <T> SortedSet<T> toSortedSet(Enumerable<T> enumerable, Comparator<T> comparator) {
TreeSet<T> rt = new TreeSet<T>(comparator);
rt.addAll(enumerable.toSet());
return rt;
}
private void assertComplex() {
if (values.length == 1)
throw new RuntimeException("Single-valued key cannot be represented as a complex value");
}
/**
* Gets the entity-key type: SINGLE or COMPLEX.
*
* @return SINGLE or COMPLEX
*/
public KeyType getKeyType() {
return values.length == 1 ? KeyType.SINGLE : KeyType.COMPLEX;
}
private static OProperty<?> getProp(List<OProperty<?>> props, String name) {
for (OProperty<?> prop : props)
if (prop.getName().equals(name))
return prop;
throw new IllegalArgumentException(String.format(
"Property %s not found in %s", name, props));
}
private static Object[] validate(Object[] values) {
if (values == null)
throw new IllegalArgumentException("Key values cannot be null");
for (Object value : values)
if (value == null)
throw new IllegalArgumentException("Key values cannot be null");
if (values.length == 0)
throw new IllegalArgumentException("Key values cannot be empty");
if (values.length == 1) {
Object o = values[0];
if (o instanceof NamedValue)
o = ((NamedValue<?>) o).getValue();
assertSimple(o);
return new Object[] { o };
}
Object[] v = new Object[values.length];
for (int i = 0; i < values.length; i++) {
Object o = values[i];
if (!(o instanceof NamedValue<?>))
throw new IllegalArgumentException(
"Complex key values must be named");
NamedValue<?> nv = (NamedValue<?>) o;
if (nv.getName() == null || nv.getName().length() == 0)
throw new IllegalArgumentException(
"Complex key values must be named");
if (nv.getValue() == null)
throw new IllegalArgumentException(
"Complex key values cannot be null");
assertSimple(nv.getValue());
v[i] = NamedValues.copy(nv);
}
return v;
}
private static void assertSimple(Object o) {
if (!EDM_SIMPLE_JAVA_TYPES.contains(o.getClass()))
throw new IllegalArgumentException("Key value must be a simple type, found: " + o.getClass().getName());
}
private static final Set<Class<?>> EDM_SIMPLE_JAVA_TYPES =
Enumerable.create(EdmSimpleType.ALL).selectMany(new Func1<EdmSimpleType<?>, Enumerable<Class<?>>>() {
public Enumerable<Class<?>> apply(EdmSimpleType<?> input) {
return Enumerable.create(input.getJavaTypes());
}
}).toSet();
private static String keyString(Object[] values) {
String keyValue;
if (values.length == 1) {
keyValue = keyString(values[0], false);
} else {
keyValue = Enumerable.create(values)
.select(new Func1<Object, String>() {
public String apply(Object input) {
return keyString(input, true);
}
}).orderBy().join(",");
}
return "(" + keyValue + ")";
}
private static String keyString(Object keyValue, boolean includePropName) {
if (keyValue instanceof NamedValue<?>) {
NamedValue<?> namedValue = (NamedValue<?>) keyValue;
String value = keyString(namedValue.getValue(), false);
if (includePropName)
return namedValue.getName() + "=" + value;
else
return value;
}
LiteralExpression expr = Expression.literal(keyValue);
return Expression.asFilterString(expr);
}
}