/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * 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 com.amazon.carbonado.info; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import org.cojen.classfile.TypeDesc; import com.amazon.carbonado.capability.IndexInfo; import com.amazon.carbonado.Storable; import com.amazon.carbonado.util.Appender; /** * Represents an index that must be defined for a specific {@link Storable} type. * * @author Brian S O'Neill * @see com.amazon.carbonado.Index */ public class StorableIndex<S extends Storable> implements Appender { /** * Parses an index descriptor and returns an index object. * * @param desc name descriptor, as created by {@link #getNameDescriptor} * @param info info on storable type * @return index represented by descriptor * @throws IllegalArgumentException if error in descriptor syntax or if it * refers to unknown properties */ @SuppressWarnings("unchecked") public static <S extends Storable> StorableIndex<S> parseNameDescriptor (String desc, StorableInfo<S> info) throws IllegalArgumentException { String name = info.getStorableType().getName(); if (!desc.startsWith(name)) { throw new IllegalArgumentException("Descriptor starts with wrong type name: \"" + desc + "\", \"" + name + '"'); } Map<String, ? extends StorableProperty<S>> allProperties = info.getAllProperties(); List<StorableProperty<S>> properties = new ArrayList<StorableProperty<S>>(); List<Direction> directions = new ArrayList<Direction>(); boolean unique; try { int pos = name.length(); if (desc.charAt(pos++) != '~') { throw new IllegalArgumentException("Invalid syntax"); } { int pos2 = nextSep(desc, pos); String attr = desc.substring(pos, pos2); if (attr.equals("U")) { unique = true; } else if (attr.equals("N")) { unique = false; } else { throw new IllegalArgumentException("Unknown attribute"); } pos = pos2; } while (pos < desc.length()) { char sign = desc.charAt(pos++); if (sign == '+') { directions.add(Direction.ASCENDING); } else if (sign == '-') { directions.add(Direction.DESCENDING); } else if (sign == '~') { directions.add(Direction.UNSPECIFIED); } else { throw new IllegalArgumentException("Unknown property direction"); } int pos2 = nextSep(desc, pos); String propertyName = desc.substring(pos, pos2); StorableProperty<S> property = allProperties.get(propertyName); if (property == null) { throw new IllegalArgumentException("Unknown property: " + propertyName); } properties.add(property); pos = pos2; } } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException("Invalid syntax"); } int size = properties.size(); if (size == 0 || size != directions.size()) { throw new IllegalArgumentException("No properties specified"); } StorableIndex<S> index = new StorableIndex<S> (properties.toArray(new StorableProperty[size]), directions.toArray(new Direction[size])); return index.unique(unique); } /** * Find the first subsequent occurrance of '+', '-', or '~' in the string * or the end of line if none are there * @param desc string to search * @param pos starting position in string * @return position of next separator, or end of string if none present */ private static int nextSep(String desc, int pos) { int pos2 = desc.length(); // assume we'll find none int candidate = desc.indexOf('+', pos); if (candidate > 0) { pos2=candidate; } candidate = desc.indexOf('-', pos); if (candidate>0) { pos2 = Math.min(candidate, pos2); } candidate = desc.indexOf('~', pos); if (candidate>0) { pos2 = Math.min(candidate, pos2); } return pos2; } private final StorableProperty<S>[] mProperties; private final Direction[] mDirections; private final boolean mUnique; private final boolean mClustered; /** * Creates a StorableIndex from the given properties and matching * directions. Both arrays must match length. * * @throws IllegalArgumentException if any argument is null, if lengths * do not match, or if any length is zero. */ public StorableIndex(StorableProperty<S>[] properties, Direction[] directions) { this(properties, directions, false); } /** * Creates a StorableIndex from the given properties and matching * directions. Both arrays must match length. Allows specification of the * uniqueness of the index. * * @param properties * @param directions * @param unique */ public StorableIndex(StorableProperty<S>[] properties, Direction[] directions, boolean unique) { this(properties, directions, unique, false, true); } /** * Creates a StorableIndex from the given properties and matching * directions. Both arrays must match length. Allows specification of the * uniqueness of the index as well as clustered option. * * @param properties * @param directions * @param unique * @param clustered */ public StorableIndex(StorableProperty<S>[] properties, Direction[] directions, boolean unique, boolean clustered) { this(properties, directions, unique, clustered, true); } /** * The guts of it. All the calls within this class specify doClone=false. * @param properties * @param directions * @param unique * @param clustered * @param doClone */ private StorableIndex(StorableProperty<S>[] properties, Direction[] directions, boolean unique, boolean clustered, boolean doClone) { if (properties == null || directions == null) { throw new IllegalArgumentException(); } if (properties.length != directions.length) { throw new IllegalArgumentException(); } if (properties.length < 1) { throw new IllegalArgumentException(); } mProperties = doClone ? properties.clone() : properties; mDirections = doClone ? directions.clone() : directions; mUnique = unique; mClustered = clustered; } /** * Creates a StorableIndex from a StorableKey. * * @param direction optional direction to apply to each key property that * has an unspecified direction * @throws IllegalArgumentException if key is null or it has * no properties */ @SuppressWarnings("unchecked") public StorableIndex(StorableKey<S> key, Direction direction) { if (key == null) { throw new IllegalArgumentException(); } Set<? extends OrderedProperty<S>> properties = key.getProperties(); if (properties.size() < 1) { throw new IllegalArgumentException(); } if (direction == null) { direction = Direction.UNSPECIFIED; } mProperties = new StorableProperty[properties.size()]; mDirections = new Direction[properties.size()]; int i = 0; for (OrderedProperty<S> prop : properties) { mProperties[i] = prop.getChainedProperty().getPrimeProperty(); if (prop.getDirection() == Direction.UNSPECIFIED) { mDirections[i] = direction; } else { mDirections[i] = prop.getDirection(); } i++; } mUnique = true; mClustered = false; } /** * Creates a StorableIndex from OrderedProperties. * * @param direction optional direction to apply to each property that * has an unspecified direction * @throws IllegalArgumentException if no properties supplied */ @SuppressWarnings("unchecked") public StorableIndex(OrderedProperty<S>[] properties, Direction direction) { if (properties == null || properties.length == 0) { throw new IllegalArgumentException(); } if (direction == null) { direction = Direction.UNSPECIFIED; } mProperties = new StorableProperty[properties.length]; mDirections = new Direction[properties.length]; int i = 0; for (OrderedProperty<S> prop : properties) { mProperties[i] = prop.getChainedProperty().getPrimeProperty(); if (prop.getDirection() == Direction.UNSPECIFIED) { mDirections[i] = direction; } else { mDirections[i] = prop.getDirection(); } i++; } mUnique = false; mClustered = false; } /** * Creates a StorableIndex from an IndexInfo. * * @param type type of storable index is defined for * @param indexInfo IndexInfo returned from storage object * @throws IllegalArgumentException if any argument is null, if any * properties are invalid, or if index info has no properties */ @SuppressWarnings("unchecked") public StorableIndex(Class<S> type, IndexInfo indexInfo) { if (indexInfo == null) { throw new IllegalArgumentException(); } Map<String, ? extends StorableProperty<S>> allProperties = StorableIntrospector.examine(type).getAllProperties(); String[] propertyNames = indexInfo.getPropertyNames(); if (propertyNames.length == 0) { throw new IllegalArgumentException("No properties in index info"); } mProperties = new StorableProperty[propertyNames.length]; for (int i=0; i<propertyNames.length; i++) { StorableProperty<S> property = allProperties.get(propertyNames[i]); if (property == null) { throw new IllegalArgumentException("Property not found: " + propertyNames[i]); } mProperties[i] = property; } mDirections = indexInfo.getPropertyDirections(); mUnique = indexInfo.isUnique(); mClustered = indexInfo.isClustered(); } /** * Returns the type of storable this index applies to. */ public Class<S> getStorableType() { return getProperty(0).getEnclosingType(); } /** * Returns the count of properties in this index. */ public int getPropertyCount() { return mProperties.length; } /** * Returns a specific property in this index. */ public StorableProperty<S> getProperty(int index) { return mProperties[index]; } /** * Returns a new array with all the properties in it. */ public StorableProperty<S>[] getProperties() { return mProperties.clone(); } /** * Returns the requested direction of a specific property in this index. */ public Direction getPropertyDirection(int index) { return mDirections[index]; } /** * Returns a new array with all the property directions in it. */ public Direction[] getPropertyDirections() { return mDirections.clone(); } /** * Returns a specific property in this index, with the direction folded in. */ public OrderedProperty<S> getOrderedProperty(int index) { return OrderedProperty.get(mProperties[index], mDirections[index]); } /** * Returns a new array with all the properties in it, with directions * folded in. */ @SuppressWarnings("unchecked") public OrderedProperty<S>[] getOrderedProperties() { OrderedProperty<S>[] ordered = new OrderedProperty[mProperties.length]; for (int i=mProperties.length; --i>=0; ) { ordered[i] = OrderedProperty.get(mProperties[i], mDirections[i]); } return ordered; } public boolean isUnique() { return mUnique; } /** * Returns true if index is known to be clustered, which means it defines * the physical ordering of storables. */ public boolean isClustered() { return mClustered; } /** * Returns a StorableIndex instance which is unique or not. */ public StorableIndex<S> unique(boolean unique) { if (unique == mUnique) { return this; } return new StorableIndex<S>(mProperties, mDirections, unique, mClustered, false); } /** * Returns a StorableIndex instance which is clustered or not. */ public StorableIndex<S> clustered(boolean clustered) { if (clustered == mClustered) { return this; } return new StorableIndex<S>(mProperties, mDirections, mUnique, clustered, false); } /** * Returns a StorableIndex instance with all the properties reversed. */ public StorableIndex<S> reverse() { Direction[] directions = mDirections; specified: { for (int i=directions.length; --i>=0; ) { if (directions[i] != Direction.UNSPECIFIED) { break specified; } } // Completely unspecified direction, so nothing to reverse. return this; } directions = directions.clone(); for (int i=directions.length; --i>=0; ) { directions[i] = directions[i].reverse(); } return new StorableIndex<S>(mProperties, directions, mUnique, mClustered, false); } /** * Returns a StorableIndex instance with all unspecified directions set to * the given direction. Returns this if all directions are already * specified. * * @param direction direction to replace all unspecified directions */ public StorableIndex<S> setDefaultDirection(Direction direction) { Direction[] directions = mDirections; unspecified: { for (int i=directions.length; --i>=0; ) { if (directions[i] == Direction.UNSPECIFIED) { break unspecified; } } // Completely specified direction, so nothing to alter. return this; } directions = directions.clone(); for (int i=directions.length; --i>=0; ) { if (directions[i] == Direction.UNSPECIFIED) { directions[i] = direction; } } return new StorableIndex<S>(mProperties, directions, mUnique, mClustered, false); } /** * Returns a StorableIndex with the given property added. If this index * already contained the given property (regardless of sort direction), * this index is returned. * * @param property property to add unless already in this index * @param direction direction to apply to property, if added * @return new index with added property or this if index already contained property */ public StorableIndex<S> addProperty(StorableProperty<S> property, Direction direction) { for (int i=mProperties.length; --i>=0; ) { if (mProperties[i].equals(property)) { return this; } } StorableProperty<S>[] properties = new StorableProperty[mProperties.length + 1]; Direction[] directions = new Direction[mDirections.length + 1]; System.arraycopy(mProperties, 0, properties, 0, mProperties.length); System.arraycopy(mDirections, 0, directions, 0, mDirections.length); properties[properties.length - 1] = property; directions[directions.length - 1] = direction; return new StorableIndex<S>(properties, directions, mUnique, mClustered, false); } /** * Returns a StorableIndex which is unique, possibly by appending * properties from the given key. If index is already unique, it is * returned as-is. */ public StorableIndex<S> uniquify(StorableKey<S> key) { if (key == null) { throw new IllegalArgumentException(); } if (isUnique()) { return this; } StorableIndex<S> index = this; for (OrderedProperty<S> keyProp : key.getProperties()) { index = index.addProperty (keyProp.getChainedProperty().getPrimeProperty(), keyProp.getDirection()); } return index.unique(true); } /** * Converts this index into a parseable name descriptor string, whose * general format is: * * <p>{@code <storable type>~<attr><+|-|~><property><+|-|~><property>...} * * <p>Attr is "U" for a unique index, "N" for a non-unique index. * * <p>Example: {@code my.pkg.UserInfo~N+lastName+firstName-birthDate} * * @see #parseNameDescriptor(String, StorableInfo) */ public String getNameDescriptor() { StringBuilder b = new StringBuilder(); b.append(getStorableType().getName()); b.append('~'); b.append(isUnique() ? 'U': 'N'); int count = getPropertyCount(); for (int i=0; i<count; i++) { b.append(getPropertyDirection(i).toCharacter()); b.append(getProperty(i).getName()); } return b.toString(); } /** * Converts this index into a parseable type descriptor string, which * basically consists of Java type descriptors appended together. There is * one slight difference. Types which may be null are prefixed with a 'N' * character. */ public String getTypeDescriptor() { StringBuilder b = new StringBuilder(); int count = getPropertyCount(); for (int i=0; i<count; i++) { StorableProperty property = getProperty(i); if (property.isNullable()) { b.append('N'); } b.append(TypeDesc.forClass(property.getType()).getDescriptor()); } return b.toString(); } @Override public int hashCode() { return (mUnique ? 0 : 31) + Arrays.hashCode(mProperties) * 31 + Arrays.hashCode(mDirections); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof StorableIndex) { StorableIndex<?> other = (StorableIndex<?>) obj; return isUnique() == other.isUnique() && isClustered() == other.isClustered() && Arrays.equals(mProperties, other.mProperties) && Arrays.equals(mDirections, other.mDirections); } return false; } @Override public String toString() { StringBuilder b = new StringBuilder(); b.append("StorableIndex "); try { appendTo(b); } catch (IOException e) { // Not gonna happen. } return b.toString(); } /** * Appends the same results as toString, but without the "StorableIndex" * prefix. */ public void appendTo(Appendable app) throws IOException { app.append("{properties=["); int length = mProperties.length; for (int i=0; i<length; i++) { if (i > 0) { app.append(", "); } app.append(mDirections[i].toCharacter()); app.append(mProperties[i].getName()); } app.append(']'); app.append(", unique="); app.append(String.valueOf(isUnique())); app.append('}'); } }