--- /dev/null
+package org.collectionspace.services.id.part;
+
+public abstract class AlgorithmicIDPart implements IDPart, DynamicValueIDPart {
+
+ @Override
+ public abstract IDPartOutputFormatter getOutputFormatter();
+
+ @Override
+ public abstract IDPartValidator getValidator();
+
+ public abstract IDPartAlgorithm getAlgorithm();
+
+ @Override
+ public String newID() {
+ return getOutputFormatter().format(getAlgorithm().generateID());
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+// @TODO Largely unimplemented at present.
+// Corresponding test class has not yet been created.
+
+public class AlphabeticSequenceIDPart extends SequenceIDPart {
+
+ // @TODO Externalize character sequences to their own class.
+ private AlphabeticSequenceIDPart.AlphabeticCharSequence charsInSequence;
+ private IDPartOutputFormatter formatter;
+ private IDPartValidator validator;
+ private char initialValue;
+
+ public AlphabeticSequenceIDPart () {
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter () {
+ return formatter;
+ }
+
+ public void setOutputFormatter (IDPartOutputFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ @Override
+ public boolean hasCurrentID() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public String getCurrentID() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public void setCurrentID(String s) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public String getInitialID() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public String nextID() {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ public char getInitialValue () {
+ return initialValue;
+ }
+
+ public void setInitialValue (char val) {
+ this.initialValue = val;
+ }
+
+ public AlphabeticSequenceIDPart.AlphabeticCharSequence getCharsInSequence () {
+ return charsInSequence;
+ }
+
+ public void setCharsInSequence
+ (AlphabeticSequenceIDPart.AlphabeticCharSequence val) {
+ this.charsInSequence = val;
+ }
+
+ public enum AlphabeticCharSequence {
+ ;
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public abstract class DateIDPart implements IDPart, DynamicValueIDPart {
+
+ public DateIDPart () {
+ }
+
+ @Override
+ public abstract IDPartOutputFormatter getOutputFormatter();
+
+ @Override
+ public abstract IDPartValidator getValidator();
+
+ @Override
+ public abstract String newID();
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public interface DynamicValueIDPart extends IDPart {
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class GregorianDateIDPart extends DateIDPart {
+
+ private GregorianDateIDPartOutputFormatter formatter;
+ private IDPartValidator validator = IDPart.DEFAULT_VALIDATOR;
+
+ public GregorianDateIDPart(String formatPattern) {
+ setFormatter(new GregorianDateIDPartOutputFormatter(formatPattern));
+ }
+
+ public GregorianDateIDPart(String formatPattern, String languageCode) {
+ setFormatter(
+ new GregorianDateIDPartOutputFormatter(formatPattern, languageCode));
+ }
+
+ public GregorianDateIDPart(String formatPattern, IDPartValidator validator) {
+ setFormatter(new GregorianDateIDPartOutputFormatter(formatPattern));
+ setValidator(validator);
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter() {
+ return this.formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ @Override
+ public String newID() {
+
+ // Get the current time instance.
+ Calendar cal = GregorianCalendar.getInstance();
+
+ // @TODO Implement time zone awareness.
+ //
+ // Relevant time zones may include:
+ // - A time zone for the server environment.
+ // - A default time zone for the tenant.
+ // - A user-specific time zone, acquired externally
+ // (e.g. from a client when called from that context).
+ // cal.setTimeZone(TimeZone tz);
+
+ // The following value is coerced to a String, representing
+ // milliseconds in the Epoch, for conformance with the contract
+ // that output formatters act on Strings.
+ //
+ // In the formatter, this value is converted back into a date
+ // and date-specific formatting is applied.
+ return getOutputFormatter().format(Long.toString(cal.getTime().getTime()));
+ }
+
+ private void setFormatter(GregorianDateIDPartOutputFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ private void setValidator(IDPartValidator validator) {
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import java.util.Date;
+import java.util.Locale;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import java.util.Arrays;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// Uses Java's interpreter for printf-style format strings.
+// http://java.sun.com/javase/1.6/docs/api/java/util/Formatter.html
+
+public class GregorianDateIDPartOutputFormatter implements IDPartOutputFormatter {
+
+ final Logger logger =
+ LoggerFactory.getLogger(GregorianDateIDPartOutputFormatter.class);
+
+ private int maxOutputLength = DEFAULT_MAX_OUTPUT_LENGTH;
+ private Locale locale = null;
+ private String language;
+ private String formatPattern;
+
+ public GregorianDateIDPartOutputFormatter(String formatPattern) {
+ setFormatPattern(formatPattern);
+ setLocale(Locale.getDefault());
+ }
+
+ public GregorianDateIDPartOutputFormatter(String formatPattern,
+ String languageCode) {
+ setFormatPattern(formatPattern);
+ setLocale(languageCode);
+ }
+
+ @Override
+ public int getMaxOutputLength () {
+ return this.maxOutputLength;
+ }
+
+ public void setMaxOutputLength (int length) {
+ this.maxOutputLength = length;
+ }
+
+ @Override
+ public String getFormatPattern () {
+ return this.formatPattern;
+ }
+
+ public void setFormatPattern(String pattern) {
+ if (pattern == null || pattern.trim().isEmpty()) {
+ logger.error("Format pattern cannot be null or empty.");
+ } else {
+ this.formatPattern = pattern;
+ }
+ }
+
+ @Override
+ public String format(String id) {
+
+ Long millisecondsInEpoch = 0L;
+ try {
+ millisecondsInEpoch = (Long.parseLong(id));
+ } catch (NumberFormatException e) {
+ logger.error("Could not parse date milliseconds as a number.", e);
+ return "";
+ }
+
+ String formattedID = "";
+ if (millisecondsInEpoch > 0) {
+ Date d = new Date(millisecondsInEpoch);
+ formattedID = formatDate(d);
+
+ // @TODO Check for exceeding maximum length before
+ // returning formatted date value.
+ }
+
+ return formattedID;
+ }
+
+ public String formatDate(Date date) {
+ SimpleDateFormat dateformatter;
+ if (getLocale() != null) {
+ dateformatter = new SimpleDateFormat(getFormatPattern(), getLocale());
+ } else {
+ dateformatter = new SimpleDateFormat(getFormatPattern());
+ }
+ return dateformatter.format(date);
+ }
+
+ // @TODO Consider generalizing locale-specific operations
+ // in a utility class outside of ID package.
+
+ public Locale getLocale() {
+ return this.locale;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ public void setLocale(String languageCode) {
+
+ if (languageCode == null || languageCode.trim().isEmpty()) {
+ logger.error("Locale language code cannot be null or empty.");
+ return;
+ }
+
+ if (languageCode.length() != 2) {
+ logger.error(
+ "Locale language code '" + languageCode +
+ "' must be a two-letter ISO-639 language code.");
+ return;
+ }
+
+ // Although language codes are documented as required to be
+ // in lowercase, and they are output in that way in
+ // DateFormat.getAvailableLocales(), they appear to be
+ // matched - within Locales - in a case-insensitive manner.
+ /*
+ if (! languageCode.equals(languageCode.toLowerCase())) {
+ logger.error("Locale language code must be in lower case.");
+ return;
+ }
+ */
+
+ Locale l = new Locale(languageCode, "");
+ if (isValidLocaleForDateFormatter(l)) {
+ setLocale(l);
+ } else {
+ logger.error("Locale language code '" + languageCode +
+ "' cannot be used for formatting dates.");
+ return;
+ }
+ }
+
+ private boolean isValidLocaleForDateFormatter(Locale l) {
+ Locale[] locales = DateFormat.getAvailableLocales();
+ return (Arrays.asList(locales).contains(l)) ? true : false;
+ }
+
+ private boolean isValidLength(String id) {
+ if (id.length() > getMaxOutputLength()) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public interface IDPart {
+
+ IDPartOutputFormatter DEFAULT_FORMATTER = new NoOpIDPartOutputFormatter();
+ IDPartValidator DEFAULT_VALIDATOR = new NoOpIDPartValidator();
+
+ public IDPartOutputFormatter getOutputFormatter();
+
+ public IDPartValidator getValidator();
+
+ public String newID();
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public interface IDPartAlgorithm {
+
+ public String generateID();
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public interface IDPartOutputFormatter {
+
+ public static int DEFAULT_MAX_OUTPUT_LENGTH = Integer.MAX_VALUE;
+
+ public int getMaxOutputLength();
+
+ public String getFormatPattern();
+
+ public String format(String id);
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class IDPartRegexValidator implements IDPartValidator {
+
+ final Logger logger = LoggerFactory.getLogger(IDPartRegexValidator.class);
+
+ @Override
+ public boolean isValid(String id) {
+
+ if (id == null) {
+ return false;
+ }
+
+ boolean isValid = false;
+ try {
+ Pattern pattern = Pattern.compile(getRegexPattern());
+ Matcher matcher = pattern.matcher(id);
+ if (matcher.matches()) {
+ isValid = true;
+ }
+ // @TODO Validation will fail by default if the regex pattern
+ // cannot be compiled. We may wish to consider re-throwing this
+ // Exception as an IllegalStateException, to raise this issue for
+ // timely resolution.
+ } catch (PatternSyntaxException e) {
+ String regex = getRegexPattern();
+ String msg =
+ (regex == null || regex.trim().isEmpty()) ?
+ "Could not validate ID due to null or empty regex pattern." :
+ "Could not validate ID due to invalid regex pattern: " + regex;
+ logger.error(msg, e);
+ }
+
+ return isValid;
+
+ }
+
+ public abstract String getRegexPattern();
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public interface IDPartValidator {
+
+ public boolean isValid(String id);
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import java.util.IllegalFormatException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+// Uses Java's interpreter for printf-style format strings.
+// http://java.sun.com/javase/1.6/docs/api/java/util/Formatter.html
+
+public class JavaPrintfIDPartOutputFormatter implements IDPartOutputFormatter {
+
+ StringWriter stringwriter = new StringWriter();
+ private int maxOutputLength = DEFAULT_MAX_OUTPUT_LENGTH;
+ private String formatPattern;
+
+ public JavaPrintfIDPartOutputFormatter () {
+ }
+
+ @Override
+ public int getMaxOutputLength () {
+ return this.maxOutputLength;
+ }
+
+ public void setMaxOutputLength (int length) {
+ this.maxOutputLength = length;
+ }
+
+ @Override
+ public String getFormatPattern () {
+ return this.formatPattern;
+ }
+
+ public void setFormatPattern(String pattern) {
+ this.formatPattern = pattern;
+ }
+
+ @Override
+ public String format(String id) {
+
+ String formattedID = id;
+
+ String pattern = getFormatPattern();
+
+ // If the formatting pattern is empty, just check length.
+ if (pattern == null || pattern.trim().isEmpty()) {
+ isValidLength(formattedID);
+
+ // Otherwise, format the ID using the pattern, then check length.
+ } else {
+ // Clear the StringWriter's buffer from its last usage.
+ StringBuffer buf = stringwriter.getBuffer();
+ buf.setLength(0);
+ // Apply the formatting pattern to the ID.
+ try {
+ PrintWriter printwriter = new PrintWriter(stringwriter);
+ printwriter.printf(id, pattern);
+ formattedID = stringwriter.toString();
+ } catch(IllegalFormatException e) {
+ // @TODO Log and handle this exception.
+ }
+ isValidLength(formattedID);
+ }
+
+ return formattedID;
+ }
+
+ // Check whether the formatted ID exceeds the specified maximum length.
+
+ private void isValidLength(String id) {
+ if (id.length() > getMaxOutputLength()) {
+ // @TODO Log error, possibly throw exception.
+ }
+
+ }
+
+}
+
--- /dev/null
+
+package org.collectionspace.services.id.part;
+
+import java.util.Random;
+
+public class JavaRandomNumberIDPartAlgorithm implements IDPartAlgorithm {
+
+ // @TODO Verify whether this simple singleton pattern is
+ // achieving the goal of using a single instance of the random
+ // number generator.
+
+ // @TODO Check whether we might need to store a serialization
+ // of this class, once instantiated, between invocations, and
+ // load the class from its serialized state.
+
+ // @TODO Look into whether we may have some user stories or use cases
+ // that require the use of java.security.SecureRandom, rather than
+ // java.util.Random.
+
+ // Starting with Java 5, the default instantiation of Random()
+ // sets the seed "to a value very likely to be distinct from any
+ // other invocation of this constructor."
+ private Random r = new Random();
+
+ private JavaRandomNumberIDPartAlgorithm() {
+ }
+
+ // See http://en.wikipedia.org/wiki/Singleton_pattern
+ private static class SingletonHolder {
+ private static final JavaRandomNumberIDPartAlgorithm INSTANCE =
+ new JavaRandomNumberIDPartAlgorithm();
+ }
+
+ public static JavaRandomNumberIDPartAlgorithm getInstance() {
+ return SingletonHolder.INSTANCE;
+ }
+
+ // @TODO Allow setting a maximum value.
+ @Override
+ public String generateID(){
+ // Returns a value between 0 (inclusive) and the
+ // maximum value of an int.
+ return Integer.toString(r.nextInt(Integer.MAX_VALUE));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class NoOpIDPartOutputFormatter implements IDPartOutputFormatter {
+
+ public NoOpIDPartOutputFormatter () {
+ }
+
+ @Override
+ public int getMaxOutputLength () {
+ return Integer.MAX_VALUE;
+ }
+
+ public void setMaxOutputLength (int length) {
+ // Do nothing.
+ }
+
+ @Override
+ public String getFormatPattern () {
+ return "";
+ }
+
+ public void setFormatPattern(String pattern) {
+ // Do nothing.
+ }
+
+ @Override
+ public String format(String id) {
+ return id;
+ }
+
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class NoOpIDPartValidator implements IDPartValidator {
+
+ public NoOpIDPartValidator() {
+ }
+
+ @Override
+ public boolean isValid(String id) {
+ return true;
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class NonEmptyIDPartValidator implements IDPartValidator {
+
+ @Override
+ public boolean isValid(String id) {
+ if (id == null || id.trim().isEmpty()) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class NumericIDPartRegexValidator extends IDPartRegexValidator {
+
+ final static String REGEX_PATTERN = "(\\d+)";
+
+ public NumericIDPartRegexValidator() {
+ }
+
+ @Override
+ public String getRegexPattern() {
+ return REGEX_PATTERN;
+ }
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NumericSequenceIDPart extends SequenceIDPart {
+
+ final Logger logger =
+ LoggerFactory.getLogger(NumericSequenceIDPart.class);
+
+ final static private long DEFAULT_INITIAL_VALUE = 1;
+ private long initialValue = DEFAULT_INITIAL_VALUE;
+
+ final static private long CURRENT_VALUE_NOT_SET = -1;
+ private long currentValue = CURRENT_VALUE_NOT_SET;
+
+ final static private long DEFAULT_INCREMENT_BY_VALUE = 1;
+ private long incrementBy = DEFAULT_INCREMENT_BY_VALUE;
+
+ // @TODO Replace the NoOp formatter with a printf formatter.
+ private IDPartOutputFormatter formatter = new NoOpIDPartOutputFormatter();
+ private IDPartValidator validator = new NumericIDPartRegexValidator();
+
+
+ public NumericSequenceIDPart() {
+ }
+
+ public NumericSequenceIDPart(long initial) {
+ setInitialID(initial);
+ }
+
+ public NumericSequenceIDPart(long initial, long incrementBy) {
+ setInitialID(initial);
+ setIncrementBy(incrementBy);
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter() {
+ return this.formatter;
+ }
+
+ public void setOutputFormatter (IDPartOutputFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ // newID() is implemented in superclass, SequenceIDPart.
+
+ @Override
+ public boolean hasCurrentID() {
+ return (this.currentValue == CURRENT_VALUE_NOT_SET) ? false : true;
+ }
+
+ @Override
+ public String getCurrentID() {
+ return Long.toString(this.currentValue);
+ }
+
+ public void setCurrentID(long val) {
+ if (val <= 0) {
+ logger.error("Current ID value for numeric ID sequences " +
+ "must be positive.");
+ } else {
+ this.currentValue = val;
+ }
+ }
+
+ @Override
+ public void setCurrentID(String str) {
+ try {
+ setCurrentID(Long.parseLong(str));
+ } catch (NumberFormatException e) {
+ logger.error("Could not parse current ID value as a number.", e);
+ }
+ }
+
+ @Override
+ public String getInitialID() {
+ return Long.toString(this.initialValue);
+ }
+
+ public void setInitialID(long initial) {
+ if (initial <= 0) {
+ logger.error("Current ID value for numeric ID sequences " +
+ "must be positive.");
+ } else {
+ this.initialValue = initial;
+ }
+ }
+
+ @Override
+ public String nextID() {
+ // @TODO Rethink this approach soon, as we may not want
+ // to change the current value for IDs that have been
+ // provisionally issued.
+ this.currentValue = this.currentValue + this.incrementBy;
+ return getCurrentID();
+ }
+
+ public String getIncrementBy() {
+ return Long.toString(this.incrementBy);
+ }
+
+ private void setIncrementBy(long incrementBy) {
+ if (incrementBy <= 0) {
+ logger.error("Increment-by value for numeric ID sequences " +
+ "must be positive.");
+ } else {
+ this.incrementBy = incrementBy;
+ }
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class RandomNumberIDPart extends AlgorithmicIDPart {
+
+ // @TODO The name of this class may be too generic, since there
+ // may be other random number generators with different algorithms.
+
+ private IDPartOutputFormatter formatter = IDPart.DEFAULT_FORMATTER;
+ private IDPartValidator validator = new NumericIDPartRegexValidator();
+ private IDPartAlgorithm algorithm =
+ JavaRandomNumberIDPartAlgorithm.getInstance();
+
+ public RandomNumberIDPart(){
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter() {
+ return this.formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ @Override
+ public IDPartAlgorithm getAlgorithm() {
+ return this.algorithm;
+ }
+
+ // newID() is implemented in superclass, AlgorithmicIDPart.
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public abstract class SequenceIDPart implements IDPart, DynamicValueIDPart {
+
+ public SequenceIDPart () {
+ }
+
+ @Override
+ public abstract IDPartOutputFormatter getOutputFormatter();
+
+ @Override
+ public abstract IDPartValidator getValidator();
+
+ @Override
+ public String newID() {
+ if (hasCurrentID()) {
+ return getOutputFormatter().format(nextID());
+ } else {
+ return getOutputFormatter().format(getInitialID());
+ }
+ }
+
+ abstract public boolean hasCurrentID();
+
+ abstract public String getCurrentID();
+
+ abstract public void setCurrentID(String s);
+
+ abstract public String getInitialID();
+
+ abstract public String nextID();
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class StaticValueIDPart implements IDPart {
+
+ private IDPartOutputFormatter formatter;
+ private IDPartValidator validator;
+ private String initialValue;
+
+ public StaticValueIDPart() {
+ }
+
+ @Override
+ public String newID() {
+ return getInitialValue();
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter() {
+ return this.formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ public String getInitialValue() {
+ return this.initialValue;
+ }
+
+ public void setInitialValue(String val) {
+ this.initialValue = val;
+ }
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class UUIDPart extends AlgorithmicIDPart {
+
+ // @TODO The name of this class may be too generic, since
+ // there may be other UUID generators with different algorithms.
+
+ private IDPartOutputFormatter formatter = IDPart.DEFAULT_FORMATTER;
+ private IDPartValidator validator = new UUIDType4PartRegexValidator();
+ private IDPartAlgorithm algorithm = new UUIDType4IDPartAlgorithm();
+
+ public UUIDPart() {
+ }
+
+ @Override
+ public IDPartOutputFormatter getOutputFormatter () {
+ return this.formatter;
+ }
+
+ @Override
+ public IDPartValidator getValidator() {
+ return this.validator;
+ }
+
+ @Override
+ public IDPartAlgorithm getAlgorithm() {
+ return this.algorithm;
+ }
+
+ // newID() is implemented in superclass, AlgorithmicIDPart.
+
+}
+
--- /dev/null
+package org.collectionspace.services.id.part;
+
+import java.util.UUID;
+
+public class UUIDType4IDPartAlgorithm implements IDPartAlgorithm {
+
+ public UUIDType4IDPartAlgorithm(){
+ }
+
+ @Override
+ public String generateID(){
+ return UUID.randomUUID().toString();
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part;
+
+public class UUIDType4PartRegexValidator extends IDPartRegexValidator {
+
+ // @TODO The name of this class may be too generic, since
+ // there may be other UUID generators with different algorithms.
+
+ final static String REGEX_PATTERN =
+ "(" +
+ "[a-z0-9\\-]{8}" +
+ "\\-" +
+ "[a-z0-9\\-]{4}" +
+ "\\-" +
+ "4" +
+ "[a-z0-9\\-]{3}" +
+ "\\-" +
+ "[89ab]" +
+ "[a-z0-9\\-]{3}" +
+ "\\-" +
+ "[a-z0-9\\-]{12}" +
+ ")";
+
+ public UUIDType4PartRegexValidator() {
+ }
+
+ @Override
+ public String getRegexPattern() {
+ return REGEX_PATTERN;
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.GregorianDateIDPart;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GregorianDateIDPartTest {
+
+ final Logger logger =
+ LoggerFactory.getLogger(GregorianDateIDPartTest.class);
+
+ GregorianDateIDPart part;
+
+ @Test
+ public void newID() {
+
+ // @TODO Replace these hard-coded expedients, which will all fail
+ // when the current month or year doesn't match these asserted values.
+
+ part = new GregorianDateIDPart("yyyy");
+ Assert.assertEquals(part.newID(), "2009");
+
+ part = new GregorianDateIDPart("M");
+ Assert.assertEquals(part.newID(), "11");
+
+ part = new GregorianDateIDPart("MMMM");
+ Assert.assertEquals(part.newID(), "November");
+
+ part = new GregorianDateIDPart("MMMM", "fr");
+ // Month names are not capitalized in French.
+ Assert.assertEquals(part.newID(), "novembre");
+
+ }
+
+
+ @Test
+ public void format() {
+ }
+
+ @Test
+ public void isValid() {
+ part = new GregorianDateIDPart("yyyy");
+ Assert.assertTrue(part.getValidator().isValid(part.newID()));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.NoOpIDPartValidator;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class NoOpIDPartValidatorTest {
+
+ @Test
+ public void isValid() {
+ NoOpIDPartValidator validator = new NoOpIDPartValidator();
+ Assert.assertTrue(validator.isValid(null));
+ Assert.assertTrue(validator.isValid(""));
+ Assert.assertTrue(validator.isValid("any string"));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.NonEmptyIDPartValidator;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class NonEmptyIDPartValidatorTest {
+
+ NonEmptyIDPartValidator validator = new NonEmptyIDPartValidator();
+
+ @Test
+ public void isValid() {
+ Assert.assertTrue(validator.isValid("any string"));
+ }
+
+ @Test(dependsOnMethods = {"isValid"})
+ public void isValidWithInvalidValues() {
+ Assert.assertFalse(validator.isValid(null));
+ Assert.assertFalse(validator.isValid(""));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.NumericIDPartRegexValidator;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class NumericIDPartRegexValidatorTest {
+
+ NumericIDPartRegexValidator validator = new NumericIDPartRegexValidator();
+
+ @Test
+ public void isValid() {
+ Assert.assertTrue(validator.isValid("0"));
+ Assert.assertTrue(validator.isValid("5"));
+ Assert.assertTrue(validator.isValid("123456789012345"));
+ }
+
+ @Test(dependsOnMethods = {"isValid"})
+ public void isValidWithInvalidValues() {
+ Assert.assertFalse(validator.isValid(null));
+ Assert.assertFalse(validator.isValid(""));
+ Assert.assertFalse(validator.isValid("non-numeric value"));
+ Assert.assertFalse(validator.isValid("-1"));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.SequenceIDPart;
+import org.collectionspace.services.id.part.NumericSequenceIDPart;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NumericSequenceIDPartTest {
+
+ final Logger logger =
+ LoggerFactory.getLogger(NumericSequenceIDPartTest.class);
+
+ SequenceIDPart part;
+ String id;
+
+ @Test
+ public void newIDWithDefaultInitialValue() {
+ part = new NumericSequenceIDPart();
+ id = part.newID();
+ Assert.assertEquals(id, "1");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "2");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "3");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "4");
+ }
+
+ @Test
+ public void newIDWithSuppliedInitialValue() {
+ part = new NumericSequenceIDPart(100);
+ id = part.newID();
+ Assert.assertEquals(id, "100");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "101");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "102");
+ }
+
+ @Test
+ public void newIDWithIncrementByValue() {
+ part = new NumericSequenceIDPart(5,5);
+ id = part.newID();
+ Assert.assertEquals(id, "5");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "10");
+ part.setCurrentID(id);
+
+ id = part.newID();
+ Assert.assertEquals(id, "15");
+ }
+
+ @Test
+ public void format() {
+ }
+
+ @Test
+ public void isValid() {
+ part = new NumericSequenceIDPart();
+ Assert.assertTrue(part.getValidator().isValid(part.newID()));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.IDPart;
+import org.collectionspace.services.id.part.RandomNumberIDPart;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+public class RandomNumberIDPartTest {
+
+ IDPart part;
+ String firstID;
+ String secondID;
+ String thirdID;
+
+ @BeforeTest
+ public void setUp() {
+ part = new RandomNumberIDPart();
+ firstID = part.newID();
+ secondID = part.newID();
+ thirdID = part.newID();
+ }
+
+ @Test
+ public void newIDGeneratesNonRepeatingIDs() {
+ Assert.assertTrue(firstID.compareTo(secondID) != 0);
+ Assert.assertTrue(firstID.compareTo(thirdID) != 0);
+ Assert.assertTrue(secondID.compareTo(thirdID) != 0);
+ }
+
+ @Test
+ public void isValid() {
+ Assert.assertTrue(part.getValidator().isValid(firstID));
+ Assert.assertTrue(part.getValidator().isValid(secondID));
+ Assert.assertTrue(part.getValidator().isValid(thirdID));
+ }
+
+}
--- /dev/null
+package org.collectionspace.services.id.part.test;
+
+import org.collectionspace.services.id.part.IDPart;
+import org.collectionspace.services.id.part.UUIDPart;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+public class UUIDPartTest {
+
+ IDPart part;
+ String firstID;
+ String secondID;
+ String thirdID;
+
+ @BeforeTest
+ public void setUp() {
+ part = new UUIDPart();
+ firstID = part.newID();
+ secondID = part.newID();
+ thirdID = part.newID();
+ }
+
+ @Test
+ public void newIDGeneratesNonRepeatingIDs() {
+ Assert.assertTrue(firstID.compareTo(secondID) != 0);
+ Assert.assertTrue(firstID.compareTo(thirdID) != 0);
+ Assert.assertTrue(secondID.compareTo(thirdID) != 0);
+ }
+
+ @Test
+ public void isValid() {
+ Assert.assertTrue(part.getValidator().isValid(firstID));
+ Assert.assertTrue(part.getValidator().isValid(secondID));
+ Assert.assertTrue(part.getValidator().isValid(thirdID));
+ }
+
+ @Test(dependsOnMethods = {"isValid"})
+ public void isValidWithInvalidValues() {
+ Assert.assertFalse(part.getValidator().isValid(null));
+ Assert.assertFalse(part.getValidator().isValid(""));
+ Assert.assertFalse(part.getValidator().isValid("not a UUID"));
+ Assert.assertFalse(part.getValidator().isValid("12345"));
+ // Invalid character in 15th position (should be '4').
+ Assert.assertFalse(part.getValidator().isValid(
+ "4c9395a8-1669-31f9-806c-920d86e40912"));
+ // Invalid character in 20th position
+ // (should be '8', '9', 'a', or 'b').
+ Assert.assertFalse(part.getValidator().isValid(
+ "4c9395a8-1669-41f9-106c-920d86e40912"));
+ }
+
+}