/* * ToroDB * Copyright © 2014 8Kdata Technology (www.8kdata.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.torodb.core.d2r; import com.torodb.core.TableRef; import com.torodb.core.backend.IdentifierConstraints; import com.torodb.core.exceptions.SystemException; import com.torodb.core.transaction.metainf.FieldType; import com.torodb.core.transaction.metainf.MetaDatabase; import com.torodb.core.transaction.metainf.MetaDocPart; import com.torodb.core.transaction.metainf.MetaSnapshot; import org.jooq.lambda.tuple.Tuple2; import java.text.Normalizer; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Locale; import java.util.Random; import java.util.regex.Pattern; import javax.inject.Inject; public class DefaultIdentifierFactory implements IdentifierFactory { private static final int MAX_GENERATION_TIME = 10; private final IdentifierConstraints identifierConstraints; private final char separator; private final String separatorString; private final char arrayDimensionSeparator; @Inject public DefaultIdentifierFactory(IdentifierConstraints identifierConstraints) { this.identifierConstraints = identifierConstraints; this.separator = identifierConstraints.getSeparator(); this.separatorString = String.valueOf(separator); this.arrayDimensionSeparator = identifierConstraints.getArrayDimensionSeparator(); } @Override public String toDatabaseIdentifier(MetaSnapshot metaSnapshot, String database) { NameChain nameChain = new NameChain(separatorString); nameChain.add(database); IdentifierChecker uniqueIdentifierChecker = new DatabaseIdentifierChecker(metaSnapshot); return generateUniqueIdentifier(nameChain, uniqueIdentifierChecker); } @Override public String toCollectionIdentifier(MetaSnapshot metaSnapshot, String database, String collection) { NameChain nameChain = new NameChain(separatorString); nameChain.add(database); nameChain.add(collection); IdentifierChecker uniqueIdentifierChecker = new CollectionIdentifierChecker(metaSnapshot); return generateUniqueIdentifier(nameChain, uniqueIdentifierChecker); } @Override public String toDocPartIdentifier(MetaDatabase metaDatabase, String collection, TableRef tableRef) { NameChain nameChain = new NameChain(separatorString); nameChain.add(collection); append(nameChain, tableRef); IdentifierChecker uniqueIdentifierChecker = new TableIdentifierChecker(metaDatabase); return generateUniqueIdentifier(nameChain, uniqueIdentifierChecker); } @Override public String toFieldIdentifier(MetaDocPart metaDocPart, String field, FieldType fieldType) { NameChain nameChain = new NameChain(separatorString); nameChain.add(field); IdentifierChecker uniqueIdentifierChecker = new FieldIdentifierChecker(metaDocPart); return generateUniqueIdentifier(nameChain, uniqueIdentifierChecker, String.valueOf( identifierConstraints.getFieldTypeIdentifier(fieldType))); } @Override public String toFieldIdentifierForScalar(FieldType fieldType) { return identifierConstraints.getScalarIdentifier(fieldType); } @Override public String toIndexIdentifier(MetaDatabase metaDatabase, String tableName, Iterable<Tuple2<String, Boolean>> columns) { NameChain nameChain = new NameChain(separatorString); nameChain.add(tableName); for (Tuple2<String, Boolean> column : columns) { nameChain.add(column.v1()); nameChain.add(column.v2() ? "a" : "d"); } IdentifierChecker identifierChecker = new IndexIdentifierChecker(metaDatabase); return generateUniqueIdentifier(nameChain, identifierChecker, "idx"); } private String generateUniqueIdentifier(NameChain nameChain, IdentifierChecker uniqueIdentifierChecker) { return generateUniqueIdentifier(nameChain, uniqueIdentifierChecker, null); } private String generateUniqueIdentifier(NameChain nameChain, IdentifierChecker identifierChecker, String extraImmutableName) { final Instant beginInstant = Instant.now(); final int maxSize = identifierConstraints.identifierMaxSize(); String lastCollision = null; ChainConverterFactory straightConverterFactory = ChainConverterFactory.straight; Counter counter = new Counter(); String identifier = buildIdentifier(nameChain, straightConverterFactory.getConverters(), maxSize, counter, identifierChecker, extraImmutableName); if (identifier.length() <= maxSize && identifierChecker.isUnique(identifier)) { return identifier; } if (identifier.length() <= maxSize) { lastCollision = identifier; } ChainConverterFactory counterChainConverterFactory = ChainConverterFactory.counter; NameConverter[] counterConverters = counterChainConverterFactory.getConverters(); while (ChronoUnit.SECONDS.between(beginInstant, Instant.now()) < MAX_GENERATION_TIME) { identifier = buildIdentifier(nameChain, counterConverters, maxSize, counter, identifierChecker, extraImmutableName); if (identifier.length() > maxSize) { throw new SystemException("Counter generator did not fit in maxSize!"); } if (identifierChecker.isUnique(identifier)) { return identifier; } lastCollision = identifier; counter.increment(); } if (lastCollision != null) { throw new SystemException( "Identifier collision(s) does not allow to generate a valid identifier. Last " + "collisioned identifier: " + lastCollision + ". Name chain: " + nameChain); } throw new SystemException("Can not generate a valid identifier. Name chain: " + nameChain); } private void append(NameChain nameChain, TableRef tableRef) { if (tableRef.isRoot()) { return; } TableRef parentTableRef = tableRef.getParent().get(); String name = tableRef.getName(); if (tableRef.isInArray()) { while (parentTableRef.isInArray()) { parentTableRef = parentTableRef.getParent().get(); } name = parentTableRef.getName() + arrayDimensionSeparator + tableRef.getArrayDimension(); parentTableRef = parentTableRef.getParent().get(); } append(nameChain, parentTableRef); nameChain.add(name); } private String buildIdentifier(NameChain nameChain, NameConverter[] converters, int maxSize, Counter counter, IdentifierChecker identifierChecker, String extraImmutableName) { final int nameMaxSize = extraImmutableName != null ? maxSize - extraImmutableName.length() - 1 : maxSize; StringBuilder middleIdentifierBuilder = new StringBuilder(); final int size = nameChain.size(); int index = 1; for (; index < size - 2; index++) { if (converters[1].convertWhole()) { middleIdentifierBuilder.append(nameChain.get(index)); } else { middleIdentifierBuilder.append(converters[1].convert(nameChain.get(index), separator, nameMaxSize, counter)); } middleIdentifierBuilder.append('_'); } if (index < size - 1) { if (converters[1].convertWhole()) { middleIdentifierBuilder.append(nameChain.get(index)); } else { middleIdentifierBuilder.append(converters[1].convert(nameChain.get(index), separator, nameMaxSize, counter)); } index++; } StringBuilder identifierBuilder = new StringBuilder(); if (converters[0].convertWhole()) { if (converters[0] == converters[1] && converters[0] == converters[2]) { StringBuilder intermediateIdentifierBuilder = new StringBuilder(); intermediateIdentifierBuilder.append(nameChain.get(0)); if (middleIdentifierBuilder.length() > 0) { intermediateIdentifierBuilder.append(separator); intermediateIdentifierBuilder.append(middleIdentifierBuilder); } if (index < size) { intermediateIdentifierBuilder.append(separator); intermediateIdentifierBuilder.append(nameChain.get(size - 1)); } identifierBuilder.append(converters[0].convert(intermediateIdentifierBuilder.toString(), separator, nameMaxSize, counter)); } else if (converters[0] == converters[1]) { StringBuilder intermediateIdentifierBuilder = new StringBuilder(); intermediateIdentifierBuilder.append(nameChain.get(0)); if (middleIdentifierBuilder.length() > 0) { intermediateIdentifierBuilder.append(separator); intermediateIdentifierBuilder.append(middleIdentifierBuilder); } identifierBuilder.append(converters[0].convert(intermediateIdentifierBuilder.toString(), separator, nameMaxSize, counter)); if (index < size) { identifierBuilder.append(separator); identifierBuilder.append(converters[2].convert(nameChain.get(size - 1), separator, nameMaxSize, counter)); } } else { identifierBuilder.append(converters[0].convert(nameChain.get(0), separator, nameMaxSize, counter)); if (middleIdentifierBuilder.length() > 0) { identifierBuilder.append(separator); identifierBuilder.append(middleIdentifierBuilder); } if (index < size) { identifierBuilder.append(separator); identifierBuilder.append(converters[2].convert(nameChain.get(size - 1), separator, nameMaxSize, counter)); } } } else if (converters[1].convertWhole()) { if (converters[1] == converters[2]) { identifierBuilder.append(converters[0].convert(nameChain.get(0), separator, nameMaxSize, counter)); StringBuilder intermediateIdentifierBuilder = new StringBuilder(); if (middleIdentifierBuilder.length() > 0) { intermediateIdentifierBuilder.append(middleIdentifierBuilder); } if (index < size) { intermediateIdentifierBuilder.append(separator); intermediateIdentifierBuilder.append(nameChain.get(size - 1)); } if (intermediateIdentifierBuilder.length() > 0) { identifierBuilder.append(separator); identifierBuilder.append(converters[1].convert(intermediateIdentifierBuilder.toString(), separator, nameMaxSize, counter)); } } else { identifierBuilder.append(converters[0].convert(nameChain.get(0), separator, nameMaxSize, counter)); if (middleIdentifierBuilder.length() > 0) { identifierBuilder.append(separator); identifierBuilder.append(converters[1].convert(middleIdentifierBuilder.toString(), separator, nameMaxSize, counter)); } if (index < size) { identifierBuilder.append(separator); identifierBuilder.append(nameChain.get(size - 1)); } } } else { identifierBuilder.append(converters[0].convert(nameChain.get(0), separator, nameMaxSize, counter)); if (middleIdentifierBuilder.length() > 0) { identifierBuilder.append(separator); identifierBuilder.append(middleIdentifierBuilder); } if (index < size) { identifierBuilder.append(separator); identifierBuilder.append(nameChain.get(size - 1)); } } if (extraImmutableName != null) { identifierBuilder.append(separator).append(extraImmutableName); } String identifier = identifierBuilder.toString(); if (!identifierChecker.isAllowed(identifierConstraints, identifier)) { identifier = separator + identifier; } return identifier; } private static class NameChain { private static final Pattern NO_ALLOWED_CHAR_PATTERN = Pattern.compile("[^0-9a-z_$]"); private final String separatorString; private final ArrayList<String> names; public NameChain(String separatorString) { this.separatorString = separatorString; names = new ArrayList<>(); } public void add(String e) { e = Normalizer.normalize(e, Normalizer.Form.NFD); e = NO_ALLOWED_CHAR_PATTERN.matcher(e .toLowerCase(Locale.US)) .replaceAll(separatorString); names.add(e); } public String get(int index) { return names.get(index); } public int size() { return names.size(); } } private static final NameConverters NameConverters = new NameConverters(); private static enum ChainConverterFactory { straight(NameConverters.straight), straight_cutvowels(NameConverters.straight, NameConverters.cutvowels, NameConverters.straight), @SuppressWarnings("checkstyle:LineLength") straight_singlechar(NameConverters.straight, NameConverters.singlechar, NameConverters.straight), straight_hash(NameConverters.straight, NameConverters.hash, NameConverters.straight), first_straight_hash(NameConverters.straight, NameConverters.hash), cutvowels(NameConverters.cutvowels), cutvowels_singlechar(NameConverters.cutvowels, NameConverters.singlechar, NameConverters.cutvowels), cutvowels_hash(NameConverters.cutvowels, NameConverters.hash, NameConverters.cutvowels), first_cutvowels_hash(NameConverters.cutvowels, NameConverters.hash), hash(NameConverters.hash), hash_and_random(NameConverters.hashAndRandom), counter(NameConverters.counter); private final NameConverter[] converters; private ChainConverterFactory(NameConverter... nameConverterFactories) { this.converters = createConverters(nameConverterFactories); } public NameConverter[] getConverters() { return converters; } private NameConverter[] createConverters(NameConverter[] converterFactories) { NameConverter[] converters = new NameConverter[3]; converters[0] = converterFactories[0]; converters[1] = converters[0]; converters[2] = converters[0]; if (converterFactories.length > 1) { if (converterFactories[0] == converterFactories[1]) { converters[1] = converters[0]; } else { converters[1] = converterFactories[1]; } converters[2] = converters[0]; } if (converterFactories.length > 2) { if (converterFactories[0] == converterFactories[2]) { converters[2] = converters[0]; } else if (converterFactories[1] == converterFactories[2]) { converters[2] = converters[1]; } else { converters[2] = converterFactories[2]; } } return converters; } } private static class NameConverters { public NameConverter straight = new NameConverter() { @Override public String convert(String name, char separator, int maxSize, Counter counter) { return name; } }; public NameConverter cutvowels = new NameConverter() { private final Pattern pattern = Pattern.compile("([a-z])[aeiou]+"); @Override public String convert(String name, char separator, int maxSize, Counter counter) { return pattern.matcher(name).replaceAll("$1"); } }; public NameConverter singlechar = new NameConverter() { @Override public String convert(String name, char separator, int maxSize, Counter counter) { if (name.isEmpty()) { return name; } return "" + name.charAt(0); } }; public NameConverter hash = new NameConverter() { @Override public String convert(String name, char separator, int maxSize, Counter counter) { String value = separator + 'x' + Integer.toHexString(name.hashCode()); if (name.length() + value.length() < maxSize) { return name + value; } int availableSize = Math.min(name.length(), maxSize) - value.length(); return name.substring(0, availableSize / 2 + availableSize % 2) + name.substring(name.length() - availableSize / 2, name.length()) + value; } @Override public boolean convertWhole() { return true; } }; public NameConverter hashAndRandom = new NameConverter() { private final Random random = new Random(); @Override public String convert(String name, char separator, int maxSize, Counter counter) { String value = separator + 'x' + Integer.toHexString(name.hashCode()) + separator + 'r' + Integer.toHexString(random.nextInt()); if (name.length() + value.length() < maxSize) { return name + value; } int availableSize = Math.min(name.length(), maxSize) - value.length(); return name.substring(0, availableSize / 2 + availableSize % 2) + name.substring(name.length() - availableSize / 2, name.length()) + value; } @Override public boolean convertWhole() { return true; } }; public NameConverter counter = new NameConverter() { @Override public String convert(String name, char separator, int maxSize, Counter counter) { String value = separator + String.valueOf(counter.get()); if (name.length() + value.length() < maxSize) { return name + value; } int availableSize = Math.min(name.length(), maxSize) - value.length(); return name.substring(0, availableSize / 2 + availableSize % 2) + name.substring(name.length() - availableSize / 2, name.length()) + value; } @Override public boolean convertWhole() { return true; } }; } private abstract static class NameConverter { public abstract String convert(String name, char separator, int maxSize, Counter counter); public boolean convertWhole() { return false; } } private static class Counter { private int counter = 1; public int get() { return counter; } public void increment() { counter++; } } private static interface IdentifierChecker { boolean isUnique(String identifier); boolean isAllowed(IdentifierConstraints identifierInterface, String identifier); } private static class DatabaseIdentifierChecker implements IdentifierChecker { private final MetaSnapshot metaSnapshot; public DatabaseIdentifierChecker(MetaSnapshot metaSnapshot) { super(); this.metaSnapshot = metaSnapshot; } @Override public boolean isUnique(String identifier) { return metaSnapshot.getMetaDatabaseByIdentifier(identifier) == null; } @Override public boolean isAllowed(IdentifierConstraints identifierInterface, String identifier) { return identifierInterface.isAllowedSchemaIdentifier(identifier); } } private static class CollectionIdentifierChecker implements IdentifierChecker { private final MetaSnapshot metaSnapshot; public CollectionIdentifierChecker(MetaSnapshot metaSnapshot) { super(); this.metaSnapshot = metaSnapshot; } @Override public boolean isUnique(String identifier) { return metaSnapshot.streamMetaDatabases().noneMatch(metaDatabase -> metaDatabase .getMetaCollectionByIdentifier(identifier) != null); } @Override public boolean isAllowed(IdentifierConstraints identifierInterface, String identifier) { return identifierInterface.isAllowedSchemaIdentifier(identifier); } } private static class TableIdentifierChecker implements IdentifierChecker { private final MetaDatabase metaDatabase; public TableIdentifierChecker(MetaDatabase metaDatabase) { super(); this.metaDatabase = metaDatabase; } @Override public boolean isUnique(String identifier) { boolean noDocPartCollision = metaDatabase.streamMetaCollections() .allMatch(collection -> collection.getMetaDocPartByIdentifier(identifier) == null); boolean noIndexCollision = metaDatabase.streamMetaCollections() .flatMap(collection -> collection.streamContainedMetaDocParts()) .allMatch(docPart -> docPart.getMetaDocPartIndexByIdentifier(identifier) == null); return noDocPartCollision && noIndexCollision; } @Override public boolean isAllowed(IdentifierConstraints identifierInterface, String identifier) { return identifierInterface.isAllowedTableIdentifier(identifier); } } private static class FieldIdentifierChecker implements IdentifierChecker { private final MetaDocPart metaDocPart; public FieldIdentifierChecker(MetaDocPart metaDocPart) { super(); this.metaDocPart = metaDocPart; } @Override public boolean isUnique(String identifier) { return metaDocPart.getMetaFieldByIdentifier(identifier) == null; } @Override public boolean isAllowed(IdentifierConstraints identifierInterface, String identifier) { return identifierInterface.isAllowedColumnIdentifier(identifier); } } private static class IndexIdentifierChecker implements IdentifierChecker { private final MetaDatabase metaDatabase; public IndexIdentifierChecker(MetaDatabase metaDatabase) { super(); this.metaDatabase = metaDatabase; } @Override public boolean isUnique(String identifier) { boolean noDocPartCollision = metaDatabase.streamMetaCollections() .allMatch(collection -> collection.getMetaDocPartByIdentifier(identifier) == null); boolean noIndexCollision = metaDatabase.streamMetaCollections() .flatMap(collection -> collection.streamContainedMetaDocParts()) .allMatch(docPart -> docPart.getMetaDocPartIndexByIdentifier(identifier) == null); return noDocPartCollision && noIndexCollision; } @Override public boolean isAllowed(IdentifierConstraints identifierInterface, String identifier) { return identifierInterface.isAllowedIndexIdentifier(identifier); } } }