package org.exist.fluent; import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.exist.collections.Collection; import org.exist.xmldb.XmldbURI; /** * An actual or virtual name for a document, augmented with instructions for processing * in case of duplication. * * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a> */ public abstract class Name { protected static final Random rand = new Random(); protected String givenName, oldName; protected Collection context; private Name() {} Folder stripPathPrefix(Folder base) { return base; } private static abstract class SpecifiedName extends Name { protected String specifiedName; SpecifiedName(String specifiedName) { this.specifiedName = specifiedName; } Folder stripPathPrefix(Folder base) { int k = specifiedName.lastIndexOf('/'); if (k == -1) { return base; } else { Folder target = base.children().create(specifiedName.substring(0, k)); specifiedName = specifiedName.substring(k+1); return target; } } } void setOldName(String oldName) {assert this.oldName == null; this.oldName = oldName;} void setContext(Collection context) {assert this.context == null; this.context = context;} /** * Get the computed value of this name. Once computed, the value is cached. * * @return the value of this name */ public String get() { if (givenName == null) eval(); return givenName; } protected abstract void eval(); protected abstract String def(); @Override public String toString() { StringBuilder buf = new StringBuilder(); if (givenName != null) buf.append(givenName).append(" "); buf.append("{"); buf.append(def()); buf.append("}"); return buf.toString(); } protected boolean existsInContext(String proposedName) { if (proposedName == null || proposedName.length() == 0) throw new IllegalArgumentException("name null or empty"); XmldbURI proposedUri = XmldbURI.create(proposedName); return context.hasDocument(proposedUri) || context.hasSubcollection(proposedUri); } protected void evalInsert(String proposedName) { if (existsInContext(proposedName)) throw new DatabaseException("entry with name " + proposedName + " already exists in destination"); givenName = proposedName; } private static final Pattern NAME_PATTERN = Pattern.compile("(.*)($[0-9a-z]+)?(\\..+)?"); protected void evalDeconflict(String proposedName) { if (!existsInContext(proposedName)) { givenName = proposedName; return; } Matcher matcher = NAME_PATTERN.matcher(proposedName); boolean matchResult = matcher.matches(); assert matchResult; String baseName = matcher.group(1); if (baseName.length() > 0) baseName += "$"; String suffix = matcher.group(3); if (suffix == null) suffix = ""; evalGenerate(baseName, suffix); } protected void evalGenerate(String baseName, String suffix) { synchronized(rand) { do { givenName = baseName + Integer.toHexString(rand.nextInt()) + suffix; } while (existsInContext(givenName)); } } /** * Generate a random name that will not conflict with anything else in the destination folder. * * @return a random name unique within the target folder */ public static Name generate() { return generate(""); } /** * Generate a random name with the given suffix that will not conflict with anything else in * the destination folder. * * @param suffix the string to append to the random name, e.g. ".xml" * @return a random name unique within the target folder and ending with the given suffix */ public static Name generate(final String suffix) { return new Name() { @Override protected void eval() {evalGenerate("", suffix);} @Override public String def() {return "generate" + (suffix.length() == 0 ? "" : " " + suffix);} }; } /** * Keep the existing name of the source item if it is unique in the destination folder, otherwise * adjust it as per the rules of {@link #adjust(String)}. * * @return the existing name if unique, otherwise a unique variation of the existing name */ public static Name keepAdjust() { return new Name() { @Override protected void eval() {evalDeconflict(oldName);} @Override protected String def() {return "adjust " + (oldName == null ? "old name" : oldName);} }; } /** * Try to use the given name but, if it conflicts with anything in the destination folder, add * a random component that will make it unique. If a random component needs to be added, * it is inserted between the main part of the name and its dotted suffix, separated by * a '$' sign. (If the name has no suffix, the random component is simply appended to the * name.) If the given name already has a random component in the format described (perhaps * resulting from a previous 'adjustment'), it is first removed before a new one is selected. * * @param name the desired name * @return if the given name is unique, the name; otherwise, a unique variation on the given name */ public static Name adjust(String name) { return new SpecifiedName(name) { @Override protected void eval() {evalDeconflict(specifiedName);} @Override protected String def() {return "adjust " + specifiedName;} }; } /** * Keep the existing name of the source item, overwriting any document with the same name * in the destination folder as per the rules for {@link #overwrite(String)}. * * @return the existing name that will be used whether it's unique or not */ public static Name keepOverwrite() { return new Name() { @Override protected void eval() {givenName = oldName;} @Override protected String def() {return "overwrite " + (oldName == null ? "old name" : oldName);} }; } /** * Use the given name whether it is unique or not. If the name is already used for another * document in the destination folder, that document will be overwritten. If the name is already * used for a child folder of the destination folder, the folder will not be affected and the * operation will throw an exception. * * @param name the desired name * @return the desired name that will be used whether it's unique or not */ public static Name overwrite(String name) { return new SpecifiedName(name) { @Override protected void eval() {givenName = specifiedName;} @Override protected String def() {return "overwrite " + specifiedName;} }; } /** * Keep the existing name of the source item, believed to be unique in the destination folder. * The name follows the rules given in {@link #create(String)}. * * @return the existing name, with a stipulation that any operation using it will fail if it's a duplicate */ public static Name keepCreate() { return new Name() { @Override protected void eval() {evalInsert(oldName);} @Override protected String def() {return "create " + (oldName == null ? "old name" : oldName);} }; } /** * Use the given name that is believed to be unique. If the given name is already used in the * destination folder, the existing owner of the name will not be affected and the operation * will throw an exception. * * @param name the desired name believed to be unique * @return the desired name, with a stipulation that any operation using it will fail if it's a duplicate */ public static Name create(String name) { return new SpecifiedName(name) { @Override protected void eval() {evalInsert(specifiedName);} @Override protected String def() {return "create " + specifiedName;} }; } }