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); } }