/* * Copyright © 2015-2016 Cask Data, Inc. * * 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 co.cask.cdap.proto.id; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.element.EntityType; import java.util.Arrays; import java.util.Iterator; import java.util.Objects; import java.util.Vector; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * Uniquely identifies a particular instance of an element. * * <p> * When adding a new type of {@link EntityId}, the following must be done: * <ol> * <li> * Implement interfaces * <ol> * <li>{@link NamespacedId} if the new ID belongs to a namespace</li> * <li>{@link ParentedId} if the new ID has a parent ID</li> * </ol> * </li> * <li> * Add methods * <ol> * <li>{@code equals()} and {@code hashCode()}, using the implementations in {@link EntityId}</li> * <li>{@code fromString(String string)}, delegating to {@link EntityId#fromString(String, Class)}</li> * </ol> * </li> * <li> * Create a corresponding child class of the old {@link Id} * (once {@link Id} is removed, this is no longer needed) * </li> * <li> * Add a new entry to {@link EntityType}, associating the {@link EntityType} * with both the new {@link EntityId} and old {@link Id} * </li> * </ol> * </p> */ @SuppressWarnings("unchecked") public abstract class EntityId implements IdCompatible { private static final String IDSTRING_TYPE_SEPARATOR = ":"; private static final String IDSTRING_PART_SEPARATOR = "."; private static final Pattern IDSTRING_PART_SEPARATOR_PATTERN = Pattern.compile("\\."); // Only allow alphanumeric and _ character for namespace private static final Pattern namespacePattern = Pattern.compile("[a-zA-Z0-9_]+"); // Allow hyphens for other ids. private static final Pattern idPattern = Pattern.compile("[a-zA-Z0-9_-]+"); // Allow '.' and '$' for dataset ids since they can be fully qualified class names private static final Pattern datasetIdPattern = Pattern.compile("[$\\.a-zA-Z0-9_-]+"); protected static boolean isValidNamespaceId(String name) { return namespacePattern.matcher(name).matches(); } protected static boolean isValidId(String name) { return idPattern.matcher(name).matches(); } protected static boolean isValidDatasetId(String datasetId) { return datasetIdPattern.matcher(datasetId).matches(); } private final EntityType entity; private Vector<EntityId> hierarchy; protected EntityId(EntityType entity) { this.entity = entity; } protected abstract Iterable<String> toIdParts(); public final EntityType getEntity() { return entity; } public static <T extends Id> T fromStringOld(String string, Class<T> oldIdClass) { EntityType type = EntityType.valueOfOldIdClass(oldIdClass); EntityId id = fromString(string, type.getIdClass()); return (T) id.toId(); } public static <T extends EntityId> T fromString(String string) { return fromString(string, null); } protected static <T extends EntityId> T fromString(String string, @Nullable Class<T> idClass) { String[] typeAndId = string.split(IDSTRING_TYPE_SEPARATOR, 2); if (typeAndId.length != 2) { throw new IllegalArgumentException( String.format("Expected type separator '%s' to be in the ID string: %s", IDSTRING_TYPE_SEPARATOR, string)); } String typeString = typeAndId[0]; EntityType type = EntityType.valueOf(typeString.toUpperCase()); if (type == null) { throw new IllegalArgumentException("Invalid element type: " + typeString); } if (idClass != null && !type.getIdClass().equals(idClass)) { throw new IllegalArgumentException(String.format("Expected EntityId of class '%s' but got '%s'", idClass.getName(), type.getIdClass().getName())); } String idString = typeAndId[1]; try { return type.fromIdParts(Arrays.asList(IDSTRING_PART_SEPARATOR_PATTERN.split(idString))); } catch (IllegalArgumentException e) { String message = idClass == null ? String.format("Invalid ID: %s", string) : String.format("Invalid ID for type '%s': %s", idClass.getName(), string); throw new IllegalArgumentException(message, e); } } @Override public final String toString() { StringBuilder result = new StringBuilder(); result.append(entity.name().toLowerCase()); String separator = IDSTRING_TYPE_SEPARATOR; for (String part : toIdParts()) { result.append(separator).append(part); separator = IDSTRING_PART_SEPARATOR; } return result.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } EntityId entityId = (EntityId) o; return Objects.equals(entity, entityId.entity); } @Override public int hashCode() { return Objects.hash(entity); } protected static String next(Iterator<String> iterator, String fieldName) { if (!iterator.hasNext()) { throw new IllegalArgumentException("Missing field: " + fieldName); } return iterator.next(); } protected static String nextAndEnd(Iterator<String> iterator, String fieldName) { String result = next(iterator, fieldName); if (iterator.hasNext()) { throw new IllegalArgumentException( String.format("Expected end after field '%s' but got: %s", fieldName, remaining(iterator))); } return result; } protected static String remaining(Iterator<String> iterator, @Nullable String fieldName) { if (fieldName != null && !iterator.hasNext()) { throw new IllegalArgumentException("Missing field: " + fieldName); } StringBuilder result = new StringBuilder(); String separator = ""; while (iterator.hasNext()) { result.append(separator).append(iterator.next()); separator = IDSTRING_PART_SEPARATOR; } return result.toString(); } private static String remaining(Iterator<String> iterator) { return remaining(iterator, null); } public Iterable<EntityId> getHierarchy() { if (hierarchy == null) { Vector<EntityId> hierarchy = new Vector<>(); EntityId current = this; while (current instanceof ParentedId) { hierarchy.insertElementAt(current, 0); current = ((ParentedId) current).getParent(); } hierarchy.insertElementAt(current, 0); this.hierarchy = hierarchy; } return hierarchy; } }