/* * 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.impl.id; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.UUID; import org.apache.commons.lang.StringEscapeUtils; import org.lilyproject.bytes.api.DataInput; import org.lilyproject.bytes.impl.DataInputImpl; import org.lilyproject.repository.api.AbsoluteRecordId; import org.lilyproject.repository.api.IdGenerator; import org.lilyproject.repository.api.RecordId; import org.lilyproject.repository.api.SchemaId; import org.lilyproject.util.ArgumentValidator; public class IdGeneratorImpl implements IdGenerator { protected static enum IdType { USER((byte) 0, new UserRecordIdFactory()), UUID((byte) 1, new UUIDRecordIdFactory()); private final byte identifierByte; private final RecordIdFactory factory; IdType(byte identifierByte, RecordIdFactory factory) { this.identifierByte = identifierByte; this.factory = factory; } public byte getIdentifierByte() { return identifierByte; } public RecordIdFactory getFactory() { return factory; } } private static IdType[] ID_TYPES = IdType.values(); @Override public RecordId newRecordId() { return new UUIDRecordId(this); } @Override public RecordId newRecordId(RecordId masterRecordId, Map<String, String> variantProperties) { ArgumentValidator.notNull(masterRecordId, "masterRecordId"); ArgumentValidator.notNull(variantProperties, "variantProperties"); if (!masterRecordId.isMaster()) { throw new IllegalArgumentException("Specified masterRecordId is a variant record ID."); } if (variantProperties.isEmpty()) { return masterRecordId; } checkReservedCharacters(variantProperties); return new VariantRecordId(masterRecordId, variantProperties, this); } @Override public RecordId newRecordId(Map<String, String> variantProperties) { return newRecordId(newRecordId(), variantProperties); } @Override public RecordId newRecordId(String userProvidedId) { ArgumentValidator.notNull(userProvidedId, "userProvidedId"); checkIdString(userProvidedId, "record id"); return new UserRecordId(userProvidedId, this); } @Override public RecordId newRecordId(String userProvidedId, Map<String, String> variantProperties) { return newRecordId(newRecordId(userProvidedId), variantProperties); } @Override public AbsoluteRecordId newAbsoluteRecordId(String tableName, RecordId recordId) { return new AbsoluteRecordIdImpl(tableName, recordId); } @Override public AbsoluteRecordId newAbsoluteRecordId(String tableName, String userProvided) { return newAbsoluteRecordId(tableName, newRecordId(userProvided)); } @Override public RecordId fromBytes(byte[] bytes) { return fromBytes(new DataInputImpl(bytes)); } @Override public AbsoluteRecordId absoluteFromBytes(byte[] bytes) { return AbsoluteRecordIdImpl.fromBytes(bytes, this); } @Override public RecordId fromBytes(DataInput dataInput) { byte idType = dataInput.readByte(); // Note: will throw arrayindexoutofbounds if id is not known IdType id = ID_TYPES[idType]; DataInput[] splitted = id.factory.splitInMasterAndVariant(dataInput); DataInput masterIdInput = splitted[0]; DataInput variantParamsInput = splitted[1]; RecordId masterRecordId = id.factory.fromBytes(masterIdInput, this); if (variantParamsInput != null) { return new VariantRecordId(masterRecordId, variantParamsInput, this); } else { return masterRecordId; } } // Strings // The prefix string (e.g. "UUID.") is put before the string provided by the // recordId itself protected String toString(UUIDRecordId uuidRecordId) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(IdType.UUID.name()); stringBuilder.append("."); stringBuilder.append(uuidRecordId.getBasicString()); return stringBuilder.toString(); } protected String toString(UserRecordId userRecordId) { String idString = userRecordId.getBasicString(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(IdType.USER.name()); stringBuilder.append("."); stringBuilder.append(escapeReservedCharacters(idString)); return stringBuilder.toString(); } // The variantproperties are appended to the string of the master record protected String toString(VariantRecordId variantRecordId) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(variantRecordId.getMaster().toString()); stringBuilder.append("."); boolean first = true; for (Map.Entry<String, String> entry : variantRecordId.getVariantProperties().entrySet()) { if (first) { first = false; } else { stringBuilder.append(","); } stringBuilder.append(escapeReservedCharacters(entry.getKey())).append('=') .append(escapeReservedCharacters(entry.getValue())); } return stringBuilder.toString(); } private static String[] escapedSplit(String s, char delimiter) { 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) { split.add(sb.toString()); sb = new StringBuffer(); } else if ('\\' == c) { escaped = true; sb.append(c); } else { sb.append(c); } } split.add(sb.toString()); return split.toArray(new String[0]); } @Override public RecordId fromString(String recordIdString) { String[] idParts = escapedSplit(recordIdString, '.'); if (idParts.length <= 1) { throw new IllegalArgumentException("Invalid record id, contains no dot: " + recordIdString); } String type = idParts[0].trim(); String id = idParts[1].trim(); String variantString = idParts.length > 2 ? idParts[2] : null; RecordIdFactory factory = null; for (IdType idType : ID_TYPES) { if (type.equals(idType.name())) { factory = idType.factory; break; } } if (factory == null) { throw new IllegalArgumentException("Invalid record id: unknown type '" + type + "' in record id '" + recordIdString + "'."); } checkEscapedReservedCharacters(id); RecordId masterRecordId = factory.fromString(StringEscapeUtils.unescapeJava(id), this); if (variantString == null) { return masterRecordId; } // Parse the variant string Map<String, String> variantProps = new HashMap<String, String>(); String[] variantStringParts = escapedSplit(variantString, ','); for (String part : variantStringParts) { String[] keyVal = escapedSplit(part, '='); if (keyVal.length != 2) { throw new IllegalArgumentException("Invalid record id: " + recordIdString); } String name = StringEscapeUtils.unescapeJava(keyVal[0].trim()); String value = StringEscapeUtils.unescapeJava(keyVal[1].trim()); variantProps.put(name, value); } return new VariantRecordId(masterRecordId, variantProps, this); } protected static void checkReservedCharacters(Map<String, String> props) { for (Map.Entry<String, String> entry : props.entrySet()) { checkVariantPropertyNameValue(entry.getKey()); checkVariantPropertyNameValue(entry.getValue()); } } protected static void checkVariantPropertyNameValue(String text) { checkIdString(text, "variant property name or value"); } protected static void checkIdString(String text, String what) { if (text.length() == 0) { throw new IllegalArgumentException("Zero-length " + what + " is not allowed."); } if (Character.isWhitespace(text.charAt(0)) || Character.isWhitespace(text.charAt(text.length() - 1))) { throw new IllegalArgumentException(what + " should not start or end with whitespace: \"" + text + "\"."); } checkReservedCharacters(text); } protected static void checkReservedCharacters(String text) { for (int i = 0; i < text.length(); i++) { switch (text.charAt(i)) { case '\u0000': throw new IllegalArgumentException("Null characters may not be used in a record ID"); } } } protected static void checkEscapedReservedCharacters(String text) { boolean escaped = false; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if ('\u0000' == c) { throw new IllegalArgumentException("Null characters may not be used in a record ID"); } else { if (escaped) { escaped = false; } else { switch (text.charAt(i)) { case '.': case '=': case ',': throw new IllegalArgumentException( "Reserved characters [.,=] must be escaped in a RecordID using '\\'. RecordID = " + text); case '\\': escaped = true; } } } } } protected static String escapeReservedCharacters(String text) { return text.replaceAll("([.,=\\\\])", "\\\\$1"); } @Override public SchemaId getSchemaId(byte[] id) { return new SchemaIdImpl(id); } @Override public SchemaId getSchemaId(String id) { return new SchemaIdImpl(id); } @Override public SchemaId getSchemaId(UUID id) { return new SchemaIdImpl(id); } }