]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
NOJIRA AlphabeticIDGenerator now auto-expands identifiers; allows (limited) specifica...
authorAron Roberts <aron@socrates.berkeley.edu>
Mon, 22 Jun 2009 21:56:28 +0000 (21:56 +0000)
committerAron Roberts <aron@socrates.berkeley.edu>
Mon, 22 Jun 2009 21:56:28 +0000 (21:56 +0000)
sandbox/aron/id/src/main/java/org/collectionspace/services/id/AlphabeticIDGenerator.java
sandbox/aron/id/src/main/java/org/collectionspace/services/id/AlphabeticIDPart.java
sandbox/aron/id/src/test/java/org/collectionspace/services/id/AlphabeticIDPartTest.java

index cf35682dfb3c6523a1743e5c3a5a448a13958f2f..ab22083bad3f917f9df6a2eef9cd1261e76b4af5 100644 (file)
@@ -1,13 +1,8 @@
 /*     
  * AlphabeticIDGenerator
  *
- * <p>An identifier generator that generates an incrementing ID from any composite
- * of the USASCII character sequences 'A-Z' and 'a-z', as a String object.</p>
- *
- * <p>The <code>wrap</code> property determines whether or not the sequence wraps
- * when it reaches the largest value that can be represented in <code>size</code>.
- * If <code>wrap</code> is false and the the maximum representable
- * value is exceeded, an IllegalStateException is thrown</p>
+ * <p>An identifier generator that generates an incrementing alphabetic ID
+ * from any sequence of characters, as a String object.</p>
  *
  * Copyright 2009 Regents of the University of California
  *
 
 // @TODO: Add Javadoc comments
 
-// @TODO: This still has dependencies on code and algorithms from
-// Apache Commons ID; we'll need to remove those, which will give us
-// more flexibility, most likely by:
-// * Switching to a Vector of chars, which will allow us to automatically
-//   (and optionally) expand the number of chars returned.
-// * Introducing flexibility, through configuration, in the series of
-//   characters through which this generator cycles, rather than hard-coding
-//   specific characters in the USASCII character set.
 // @TODO: The initial value determines the fixed number of characters.
+// Currently, identifiers simply 'wrap' (roll over) within that fixed
+// series, which is not always the desired outcome.
+//
 // We may also need to model cases where the number of characters
-// increases as values roll over, up to a specified maximum number of
-// characters; e.g. "z" becomes "aa", and "ZZ" becomes "AAA". When
-// doing so, we'll also need to set a maximum length to which the
-// generated IDs can grow.
-
-// @TODO: This class is hard-coded to use two series within the
-// USASCII character set.
+// auto-expands as the value of the most significant character
+// rolls over, up to a specified maximum number of characters:
+// e.g. a call to getNextID(), where the current ID is "z",
+// auto-expands to "aa", and a current ID of "ZZ" auto-expands to "AAA".
 //
-// With some minor refactoring, we could draw upon minimum and maximum
-// character values for a wide range of arbitrary character sets.
+// When doing so, we'll also need to set a maximum length to which the
+// generated IDs can grow, likely as an additional parameter to be
+// passed to a constructor, with a default value hard-coded in the class.
 
-// Some code and algorithms in the current iteration of this class
-// were adapted from the org.apache.commons.Id package, and thus
-// the relevant licensing terms are included here:
+// @TODO: Handle escaped character sequences representing Unicode code points,
+// both in the start and end characters of the sequence, and in the initial value.
+//
+// Some initial research on this:
+// http://www.velocityreviews.com/forums/t367758-unescaping-unicode-code-points-in-a-java-string.html
 
-/*
- * 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.
- */
+// NOTE: This class currently hard-codes the assumption that the values in
+// alphabetic identifiers are ordered in significance from left-to-right;
+// that is, the most significant value appears in the left-most position.
  
 package org.collectionspace.services.id;
 
-public class AlphabeticIDGenerator implements IDGenerator {
-    
-       private static final char LOWERCASE_Z_CHAR = 'z';
-       private static final char UPPERCASE_Z_CHAR = 'Z';
+import java.util.Collections;
+import java.util.Vector;
 
-       private char[] initialValue = null;
-       private char[] currentValue = null;
+public class AlphabeticIDGenerator implements IDGenerator {
+   
+  private static final char NULL_CHAR = '\u0000';
+
+  private static final String DEFAULT_START_CHAR = "a";
+  private static final String DEFAULT_END_CHAR = "z";
+         
+  private char startChar = NULL_CHAR;
+  private char endChar = NULL_CHAR;
+  
+       private Vector<Character> initialValue = new Vector<Character>();
+       private Vector<Character> currentValue = new Vector<Character>();
+
+  // Defaults to an 'a-z' series, representing lowercase alphabetic characters
+  // in the USASCII character set within Java's internal representation of
+  // characters (Unicode's UTF-16 encoding), if no start and end characters
+  // are provided for the alphabetic character sequence.
+       public AlphabeticIDGenerator(String initial) throws IllegalArgumentException {
+         
+         this(DEFAULT_START_CHAR, DEFAULT_END_CHAR, initial);
+         
+       }
        
-       public AlphabeticIDGenerator(String initialValue) throws IllegalArgumentException {
+       public AlphabeticIDGenerator(String seriesStart, String seriesEnd, String initial)
+         throws IllegalArgumentException {
+         
+         // Validate and store the start character in the alphabetic series.
+         
+         if (seriesStart == null || seriesStart.equals("")) {
+                       throw new IllegalArgumentException(
+                         "Start character in the alphabetic series must not be null or empty");
+               }
+         
+    // @TODO The next two statements will need to be revised to handle escaped
+    // representations of characters outside the USASCII character set.
+         if (seriesStart.length() > 1) {
+                       throw new IllegalArgumentException(
+                         "Start character in the alphabetic series must be exactly one character in length");
+               }
+
+    this.startChar = seriesStart.charAt(0);
 
-               if ( initialValue == null ) {
-                       throw new IllegalArgumentException("Initial value must not be null");
+         // Validate and store the end character in the alphabetic series.
+
+         if (seriesEnd == null || seriesEnd.equals("")) {
+                       throw new IllegalArgumentException(
+                         "End character in the alphabetic series must not be null or empty");
                }
-       
-               if ( initialValue == "" ) {
-                       throw new IllegalArgumentException("Initial value must not be empty");
+         
+    // @TODO The next two statements will need to be revised to handle escaped
+    // representations of characters outside the USASCII character set.
+         if (seriesEnd.length() > 1) {
+                       throw new IllegalArgumentException(
+                         "End character in the alphabetic series must be exactly one character in length");
+               }
+
+    this.endChar = seriesEnd.charAt(0);
+    
+         if (this.endChar <= this.startChar) {
+                       throw new IllegalArgumentException(
+                         "End (last) character in an alphabetic series must be greater than the start character");
+               }
+               
+         // Validate and store the initial value of this identifier.
+
+               if (initial == null || initial.equals("")) {
+                       throw new IllegalArgumentException("Initial value must not be null or empty");
                }
+               
+               // @TODO: Add a check for maximum length of the initial value here.
        
-               char[] charsToValidate = initialValue.toCharArray();
+         // Store the chars in the initial value as Characters in a Vector.
+         // (Since we're performing casts from char to Character, we can't just
+         // use Arrays.asList() to copy the initial array to a Vector.)
+               char[] chars = initial.toCharArray();
+               for (int i=0; i < chars.length; i++) {
+                 this.initialValue.add(new Character(chars[i]));
+               }
                
-               // Validate each of the characters in the initial value
-               // against ranges of valid values.
-               for (int i = 0; i < charsToValidate.length; i++) {
+               // Validate that each of the characters in the initial value
+               // falls within the provided series.
+               for ( Character ch : this.initialValue ) {
                
-                       char ch = charsToValidate[i];
-                       
-                       // If the value of the current character matches a character
-                       // in the uppercase ('A-Z') or lowercase ('a-z') series
-                       // in the USASCII character set, that character has a valid value,
-                       // so we can skip to checking the next character.
-                       if (ch >= 'A' && ch <= 'Z') continue;
-                       if (ch >= 'a' && ch <= 'z') continue;
-                       
-                       // Otherwise, we've detected a character not in those series.
-                       throw new IllegalArgumentException(
-                               "character " + charsToValidate[i] + " is not valid");
+                       if (ch.charValue() >= this.startChar && ch.charValue() <= this.endChar) {
+                         continue;
+      // Otherwise, we've detected a character not in the series.
+                       } else {
+        throw new IllegalArgumentException("character " + "\'" + ch + "\'" + " is not valid");
+      }
                                
                } // end 'for' loop
                
-               // Store the initial character array
-               this.initialValue = charsToValidate;
-               this.currentValue = charsToValidate;
+               // Initialize the current value from the initial value.
+               this.currentValue = new Vector<Character>(this.initialValue);
 
        }
 
-       // @TODO: This is still failing; we'll address this through as larger refactoring.
+  // Reset the current value to the initial value.
        public synchronized void reset() {
-               try {
-                       // TODO: Investigate using different methods to perform this copying,
-                       // such as clone.  See "Java Practices - Copy an Array"
-                       // <http://www.javapractices.com/topic/TopicAction.do?Id=3>
-                       // char [] copy = (char []) initialValue.clone();
-                       // this.currentValue = copy;
-                       // System.arraycopy( 
-                       //      this.initialValue, 0, this.currentValue, 0, this.initialValue.length );
-      for ( int i = 0; i < this.initialValue.length; ++i ) {
-        this.currentValue[i] = this.initialValue[i];
-      }
-               // If copying would cause access of data outside array bounds.
-               } catch (IndexOutOfBoundsException iobe) {
-                       // For experimentation - do nothing here at this time.
-               // If an element in the source array could not be stored into
-               // the destination array because of a type mismatch. 
-               } catch (ArrayStoreException ase) {
-                       // For experimentation - do nothing here at this time.
-               // If either source or destination is null.
-               } catch (NullPointerException npe) {
-                       // For experimentation - do nothing here at this time.
-               }
+         Collections.copy(this.currentValue, this.initialValue);
        }
 
+  // Returns the initial value.
        public synchronized String getInitialID() {
-               return new String(this.initialValue);
+               return getIDString(this.initialValue);
        }
 
+  // Returns the current value.
        public synchronized String getCurrentID() {
-               return new String(this.currentValue);
+               return getIDString(this.currentValue);
        }
        
+       // Returns the next alphabetic ID in the series.
        public synchronized String getNextID() {
-               
+                       
                // Get next values for each character, from right to left
                // (least significant to most significant).
-               //
-               // When reaching the maximum value for any character position,
-               // 'roll over' to the minimum value for that position.
-               for (int i = (this.currentValue.length - 1); i >= 0; i--) {
-               
-                       switch (this.currentValue[i]) {
-                       
-                               case LOWERCASE_Z_CHAR:  // z
-                                       if (i == 0) {
-                                               throw new IllegalStateException(
-                                                       "The maximum number of IDs has been reached");
-                                       }
-                                       this.currentValue[i] = 'a';
-                                       break;
-
-                        case UPPERCASE_Z_CHAR:  // Z
-                                       if (i == 0) {
-                                               throw new IllegalStateException(
-                                               "The maximum number of IDs has been reached");
-                                       }
-                                       this.currentValue[i] = 'A';
-                                       break;
-
-                               default:
-                                       this.currentValue[i]++;
-                                       i = -1;
-                                       break;
-                                       
-                       } // end switch
+               boolean expandIdentifier = false;
+               int size = this.currentValue.size();
+               char c;
+               for (int i = (size - 1); i >= 0; i--) {
+
+      c = this.currentValue.get(i).charValue();
+
+      // When reaching the maximum value for any character position,
+      // 'roll over' to the minimum value for that position.
+      if (c == this.endChar) {
+                   this.currentValue.set(i, Character.valueOf(this.startChar));
+                   // If this roll over occurs in the most significant value,
+                   // set a flag to later expand the size of the identifier.
+                   //
+                   // @TODO: Set another flag to enable or disable this behavior,
+                   // as well as a mechanism for setting the maximum expansion permitted.
+                   if (i == 0) {
+                     expandIdentifier = true;
+                   }
+                 } else {
+        c++;
+        this.currentValue.set(i, Character.valueOf(c));
+        i = -1;
+        break;           
+                 }
 
-               } // end 'for' loop
-   
-               return new String(currentValue);
+               }
 
+    // If we are expanding the size of the identifier, insert a new
+    // value at the most significant character position, sliding other
+    // values to the right.
+    if (expandIdentifier) {
+      this.currentValue.add(0, Character.valueOf(this.startChar));
+    }
+               
+               return getIDString(this.currentValue);
+               
   }
-       
+
+  // Returns a String representation of the contents of a Vector,
+  // in the form of an identifier (e.g. each Character's String value
+  // is appended to the next).
+  public synchronized String getIDString(Vector<Character> v) {
+               StringBuffer sb = new StringBuffer();
+         for ( Character ch : v ) {
+      sb.append(ch.toString());
+               }
+               return sb.toString();
+       }
+
 }
index 8f97a2687a75ce5cbcec38becbfc8a8da741e968..ed55d83edb41b8bb6aec0bc4460cdd43b63b0a54 100644 (file)
@@ -25,5 +25,9 @@ public class AlphabeticIDPart extends IDPart {
        public AlphabeticIDPart(String baseVal) {
                super(new AlphabeticIDGenerator(baseVal));
        };
+
+       public AlphabeticIDPart(String startVal, String endVal, String baseVal) {
+               super(new AlphabeticIDGenerator(startVal, endVal, baseVal));
+       };
                
 }
index 7599f9f296c53a7e291a2054759c3cf96d6b676e..a20ccde4685c09abc9cf85911cb64022e30e8dcb 100644 (file)
@@ -52,24 +52,24 @@ public class AlphabeticIDPartTest extends TestCase {
        
        public void testGetNextIDUppercase() {
                
-               part = new AlphabeticIDPart("A");
+               part = new AlphabeticIDPart("A", "Z", "A");
                assertEquals("B", part.getNextID());
                assertEquals("C", part.getNextID());
 
-               part = new AlphabeticIDPart("X");
+               part = new AlphabeticIDPart("A", "Z", "X");
                assertEquals("Y", part.getNextID());
                assertEquals("Z", part.getNextID());
 
-               part = new AlphabeticIDPart("AA");
+               part = new AlphabeticIDPart("A", "Z", "AA");
                assertEquals("AB", part.getNextID());
                assertEquals("AC", part.getNextID());
 
-               part = new AlphabeticIDPart("AY");
+               part = new AlphabeticIDPart("A", "Z", "AY");
                assertEquals("AZ", part.getNextID());
                assertEquals("BA", part.getNextID());
                assertEquals("BB", part.getNextID());
 
-               part = new AlphabeticIDPart("ZX");
+               part = new AlphabeticIDPart("A", "Z", "ZX");
                assertEquals("ZY", part.getNextID());
                assertEquals("ZZ", part.getNextID());
                        
@@ -87,7 +87,7 @@ public class AlphabeticIDPartTest extends TestCase {
 
        public void testResetUppercase() {
                
-               part = new AlphabeticIDPart("RA");
+               part = new AlphabeticIDPart("A", "Z", "RA");
                assertEquals("RB", part.getNextID());
                assertEquals("RC", part.getNextID());
                part.reset();
@@ -104,7 +104,7 @@ public class AlphabeticIDPartTest extends TestCase {
 
        public void testInitialUppercase() {
                
-               part = new AlphabeticIDPart("AZ");
+               part = new AlphabeticIDPart("A", "Z", "AZ");
                assertEquals("AZ", part.getInitialID());
                
        }
@@ -122,7 +122,7 @@ public class AlphabeticIDPartTest extends TestCase {
 
        public void testCurrentUppercase() {
                
-               part = new AlphabeticIDPart("A");
+               part = new AlphabeticIDPart("A", "Z", "A");
                assertEquals("A", part.getCurrentID());
                assertEquals("B", part.getNextID());
                assertEquals("C", part.getNextID());
@@ -133,31 +133,19 @@ public class AlphabeticIDPartTest extends TestCase {
        
        public void testOverflowLowercase() {
        
-               try {
-                       part = new AlphabeticIDPart("zx");
-                       assertEquals("zy", part.getNextID());
-                       assertEquals("zz", part.getNextID());
-                       // Should throw IllegalStateException
-                       assertNotNull(part.getNextID());
-                       fail("Should have thrown IllegalStateException here");
-               } catch (IllegalStateException expected) {
-                       // This Exception should be thrown, and thus the test should pass.
-               }
+    part = new AlphabeticIDPart("zx");
+    assertEquals("zy", part.getNextID());
+    assertEquals("zz", part.getNextID());
+    assertEquals("aaa", part.getNextID());
                
        }
 
        public void testOverflowUppercase() {
        
-               try {
-                       part = new AlphabeticIDPart("X");
-                       assertEquals("Y", part.getNextID());
-                       assertEquals("Z", part.getNextID());
-                       // Should throw IllegalStateException
-                       assertNotNull(part.getNextID());
-                       fail("Should have thrown IllegalStateException here");
-               } catch (IllegalStateException expected) {
-                       // This Exception should be thrown, and thus the test should pass.
-               }
+    part = new AlphabeticIDPart("A", "Z", "X");
+    assertEquals("Y", part.getNextID());
+    assertEquals("Z", part.getNextID());
+    assertEquals("AA", part.getNextID());
                
        }