implement DOI class and use in Reference #3572
authorAndreas Müller <a.mueller@bgbm.org>
Thu, 5 Sep 2013 15:06:16 +0000 (15:06 +0000)
committerAndreas Müller <a.mueller@bgbm.org>
Thu, 5 Sep 2013 15:06:16 +0000 (15:06 +0000)
.gitattributes
cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DOI.java [new file with mode: 0644]
cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UrlUtf8Coder.java [new file with mode: 0644]
cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/DoiTest.java [new file with mode: 0644]
cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/DOIUserType.java [new file with mode: 0644]
cdmlib-model/src/main/java/eu/etaxonomy/cdm/model/common/package-info.java
cdmlib-model/src/main/java/eu/etaxonomy/cdm/model/reference/IPublicationBase.java
cdmlib-model/src/main/java/eu/etaxonomy/cdm/model/reference/Reference.java
cdmlib-model/src/main/java/eu/etaxonomy/cdm/strategy/match/DefaultMatchStrategy.java

index d25b8326a961a97b2244fe733a545b624192611d..9a69f66e328302a106f5256bc8b93c48a16535be 100644 (file)
@@ -6,6 +6,7 @@ cdmlib-commons/README.TXT -text
 cdmlib-commons/pom.xml -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/AccountStore.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/CdmUtils.java -text
+cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DOI.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DocUtils.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DoubleResult.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/ExcelUtils.java -text
@@ -17,6 +18,7 @@ cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/Tree.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/TreeNode.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UTF8.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UriUtils.java -text
+cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UrlUtf8Coder.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/XmlHelp.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/media/AudioInfo.java -text
 cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/media/ImageInfo.java -text
@@ -32,6 +34,7 @@ cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/monitor/SubProgressMonitor.
 cdmlib-commons/src/main/resources/MUST-EXIST.txt -text
 cdmlib-commons/src/main/resources/log4j.properties -text
 cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/CdmUtilsTest.java -text
+cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/DoiTest.java -text
 cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/GeneralParserTest.java -text
 cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/UriUtilsTest.java -text
 cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/UuidGenerator.java -text
@@ -681,6 +684,7 @@ cdmlib-model/LICENSE.TXT -text
 cdmlib-model/README.TXT -text
 cdmlib-model/pom.xml -text
 cdmlib-model/src/main/java/eu/etaxonomy/cdm/aspectj/PropertyChangeAspect.aj -text
+cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/DOIUserType.java -text
 cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/EnumUserType.java -text
 cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/HibernateProxyHelper.java -text
 cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/PartialUserType.java -text
diff --git a/cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DOI.java b/cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/DOI.java
new file mode 100644 (file)
index 0000000..e41a8bf
--- /dev/null
@@ -0,0 +1,230 @@
+/**\r
+* Copyright (C) 2007 EDIT\r
+* European Distributed Institute of Taxonomy\r
+* http://www.e-taxonomy.eu\r
+*\r
+* The contents of this file are subject to the Mozilla Public License Version 1.1\r
+* See LICENSE.TXT at the top of this package for the full license terms.\r
+*/\r
+package eu.etaxonomy.cdm.common;\r
+\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+\r
+import org.apache.commons.lang.StringUtils;\r
+\r
+\r
+/**\r
+ * A class for handling DOIs (http://www.doi.org).\r
+ * It offers parsing and formatting functionality as well as validation.\r
+ * A {@link DOI} object can only be created by syntactic valid input.\r
+ * It internally stores a doi 2 strings, the first one being the registrant number\r
+ * (including sub numbers), the second being the suffix.\r
+ * \r
+ * \r
+ * @author a.mueller\r
+ * @created 2013-09-04\r
+ */\r
+public final class DOI implements java.io.Serializable{\r
+       \r
+       /**\r
+     * Explicit serialVersionUID for interoperability.\r
+     */\r
+       private static final long serialVersionUID = -3871039785359980553L;\r
+\r
+\r
+       /**\r
+        * The default public DOI proxy server\r
+        */\r
+       public static final String HTTP_DOI_ORG = "http://doi.org/";\r
+\r
+       /**\r
+        * The former default public DOI proxy server, still supported but no longer preferred.\r
+        * @see #HTTP_DOI_ORG\r
+        */\r
+       public static final String HTTP_OLD_DOI_ORG = "http://dx.doi.org/";\r
+       \r
+    private volatile transient int hashCode = -1;      // Zero ==> undefined\r
+\r
+       //http://www.doi.org/doi_handbook/2_Numbering.html#2.2.1\r
+//     prefix + suffix, no defined length, case-insensitive, any printable characters\r
+\r
+       \r
+//********************************* VARIABLES *************************************/   \r
+       \r
+       /**\r
+        * The directory indicator for DOIs as registered at \r
+        */\r
+       public static final String DIRECTORY_INDICATOR = "10";\r
+       private String prefix_registrantCode;\r
+\r
+       private String suffix;\r
+\r
+// ***************************** FACTORY METHODS ***************************************/\r
+       \r
+       public static DOI fromString(String doi) throws IllegalArgumentException{\r
+               return new DOI(doi);\r
+       }\r
+       \r
+       public static DOI fromRegistrantCodeAndSuffix(String registrantCode, String suffix) throws IllegalArgumentException{\r
+               return new DOI(registrantCode, suffix);\r
+       }\r
+       \r
+       \r
+// ******************************* CONSTRUCTOR ************************************/   \r
+       \r
+    /**\r
+     * Creates a doi by its registrantCode and its suffix\r
+     * @param registrantCode the registrant code, the is the part following the directoryIndicator "10." \r
+     *         and preceding the first forward slash (followed by the suffix)\r
+     * @param suffix the suffix is the part of the DOI following the first forward slash. It is provided \r
+     * by the registrant\r
+     */\r
+    private DOI(String registrantCode, String suffix) {\r
+       //preliminary until prefix_registrantCode and suffix validation is implemented\r
+               this("10." + registrantCode + "/" + suffix);\r
+               \r
+               //use only after validation of both parts\r
+//             this.prefix_registrantCode = registrantCode;\r
+//             this.suffix = suffix;\r
+       }\r
+\r
+    private DOI(String doiString) {\r
+               super();\r
+               parseDoiString(doiString);\r
+       }\r
+\r
+//************************************ GETTER ***********************************/    \r
+       \r
+       public String getPrefix() {\r
+               return makePrefix();\r
+       }\r
+    \r
+       public String getPrefix_registrantCode() {\r
+               return prefix_registrantCode;\r
+       }\r
+\r
+       public String getSuffix() {\r
+               return suffix;\r
+       }\r
+\r
+       private static Pattern doiPattern = Pattern.compile("^doi:\\s*", Pattern.CASE_INSENSITIVE); \r
+       \r
+// ********************************************* PARSER *******************************/\r
+    \r
+       private void parseDoiString(String doi){\r
+               boolean isUrn = false;\r
+               if (StringUtils.isBlank(doi)){\r
+                       throw new IllegalArgumentException("Doi string must not be null or blank");\r
+               }\r
+               doi = doi.trim();\r
+               if (doi.startsWith("https") ){\r
+                       doi = doi.replaceFirst("https", "http").trim();\r
+               }\r
+               Matcher matcher = doiPattern.matcher(doi);\r
+               if (matcher.find()){\r
+                       doi = matcher.replaceFirst("").trim();\r
+               }\r
+\r
+               \r
+               //replace URI prefix\r
+               if (doi.startsWith(HTTP_DOI_ORG)){\r
+                       doi = doi.replaceFirst(HTTP_DOI_ORG,"");\r
+               }else if (doi.startsWith(HTTP_OLD_DOI_ORG)){\r
+                       doi = doi.replaceFirst(HTTP_OLD_DOI_ORG,"");\r
+               }\r
+               \r
+               \r
+\r
+               //handle URN prefix\r
+               if (doi.startsWith("urn:doi:")){\r
+                       doi = doi.replaceFirst("urn:doi:","");\r
+                       isUrn = true;\r
+               }\r
+               \r
+               \r
+               //now we should have the pure doi\r
+               if (doi.length() > 1000){\r
+                       //for persistence reason we currently restrict the length of DOIs to 1000\r
+                       throw new IllegalArgumentException("DOIs may have a maximum length of 1000 in the CDM.");\r
+               }\r
+               \r
+               if (! doi.startsWith("10.")){\r
+                       throw new IllegalArgumentException("DOI not parsable. DOI must start with 10. or an URI or URN prefix ");\r
+               }\r
+               doi = doi.substring(3);\r
+               String sep = isUrn? ":" : "/";\r
+               \r
+//             registrant\r
+               String registrant = doi.split(sep)[0];\r
+               if (!registrant.matches("[0-9]{2,}(?:[.][0-9]+)*")){   //per definition the number of digits may also be 1, however the lowest known number is 3 so we may be on the safe side here \r
+                       String message = "Invalid prefix '10.%s'";\r
+                       throw new IllegalArgumentException(String.format(message, registrant));\r
+               }\r
+               //suffix\r
+               String suffix = doi.replaceFirst(registrant + sep,"");\r
+               if (! suffix.matches("\\p{Print}+")){\r
+                       String message = "Suffix should only include printable characters";\r
+                       throw new IllegalArgumentException(message);\r
+               }\r
+               if (isUrn){\r
+                       //TODO do some other replacements according to http://www.doi.org/doi_handbook/2_Numbering.html#2.6.3\r
+                       //e.g. slash becomes : in URN\r
+                       //TODO do we need this also for other URIs? According to http://www.doi.org/doi_handbook/2_Numbering.html#2.6 it is only required for URNs\r
+                       suffix = UrlUtf8Coder.unescape(suffix);\r
+               }\r
+               //success\r
+               this.prefix_registrantCode = registrant;\r
+               this.suffix = suffix;\r
+                       \r
+       }\r
+       \r
+       \r
+       private String makePrefix(){\r
+               return DIRECTORY_INDICATOR + "." + this.prefix_registrantCode;\r
+       }\r
+       \r
+       private String makeDoi(){\r
+               return makePrefix() + "/" + this.suffix;\r
+       }\r
+       \r
+       public String asURI(){\r
+               return HTTP_DOI_ORG + makePrefix() + "/" + uriEncodedSuffix();\r
+       }\r
+       \r
+       private String uriEncodedSuffix() {\r
+               String result = UrlUtf8Coder.encode(this.suffix);\r
+               return result;\r
+       }\r
+\r
+//************************************************* toString/equals /hashCode *********************/   \r
+\r
+       \r
+       \r
+       @Override\r
+       public int hashCode() {\r
+               if (hashCode == -1) {\r
+            hashCode = 31 * prefix_registrantCode.toUpperCase().hashCode() + suffix.toUpperCase().hashCode();\r
+        }\r
+        return hashCode;\r
+       }\r
+\r
+\r
+       @Override\r
+       public boolean equals(Object obj) {\r
+               if (obj instanceof DOI){\r
+                       DOI doi = (DOI)obj;\r
+                       if (this.prefix_registrantCode.toUpperCase().equals(doi.prefix_registrantCode.toUpperCase()) &&\r
+                                       this.suffix.toUpperCase().equals(doi.suffix.toUpperCase())){\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+\r
+       @Override\r
+       public String toString(){\r
+               return makeDoi();\r
+       }\r
+}\r
diff --git a/cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UrlUtf8Coder.java b/cdmlib-commons/src/main/java/eu/etaxonomy/cdm/common/UrlUtf8Coder.java
new file mode 100644 (file)
index 0000000..783d156
--- /dev/null
@@ -0,0 +1,189 @@
+/**\r
+ * Provides a method to encode any string into a URL-safe\r
+ * form.\r
+ * Non-ASCII characters are first encoded as sequences of\r
+ * two or three bytes, using the UTF-8 algorithm, before being\r
+ * encoded as %HH escapes.\r
+ *\r
+ * Created: 17 April 1997\r
+ * Author: Bert Bos <bert@w3.org>\r
+ *\r
+ * URLUTF8Encoder: http://www.w3.org/International/URLUTF8Encoder.java\r
+ *\r
+ * Copyright © 1997 World Wide Web Consortium, (Massachusetts\r
+ * Institute of Technology, European Research Consortium for\r
+ * Informatics and Mathematics, Keio University). All Rights Reserved. \r
+ * This work is distributed under the W3C® Software License [1] in the\r
+ * hope that it will be useful, but WITHOUT ANY WARRANTY; without even\r
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\r
+ * PURPOSE.\r
+ *\r
+ * [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231\r
+ */\r
+package eu.etaxonomy.cdm.common;\r
+\r
+public class UrlUtf8Coder{\r
+\r
+  final static String[] hex = {\r
+    "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",\r
+    "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",\r
+    "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",\r
+    "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",\r
+    "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",\r
+    "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",\r
+    "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",\r
+    "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",\r
+    "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",\r
+    "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",\r
+    "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",\r
+    "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",\r
+    "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",\r
+    "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",\r
+    "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",\r
+    "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",\r
+    "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",\r
+    "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",\r
+    "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",\r
+    "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",\r
+    "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",\r
+    "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",\r
+    "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",\r
+    "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",\r
+    "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",\r
+    "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",\r
+    "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",\r
+    "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",\r
+    "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",\r
+    "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",\r
+    "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",\r
+    "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"\r
+  };\r
+\r
+  /**\r
+   * Encode a string to the "x-www-form-urlencoded" form, enhanced\r
+   * with the UTF-8-in-URL proposal. This is what happens:\r
+   *\r
+   * <ul>\r
+   * <li><p>The ASCII characters 'a' through 'z', 'A' through 'Z',\r
+   *        and '0' through '9' remain the same.\r
+   *\r
+   * <li><p>The unreserved characters - _ . ! ~ * ' ( ) remain the same.\r
+   *\r
+   * <li><p>The space character ' ' is converted into a plus sign '+'.\r
+   *\r
+   * <li><p>All other ASCII characters are converted into the\r
+   *        3-character string "%xy", where xy is\r
+   *        the two-digit hexadecimal representation of the character\r
+   *        code\r
+   *\r
+   * <li><p>All non-ASCII characters are encoded in two steps: first\r
+   *        to a sequence of 2 or 3 bytes, using the UTF-8 algorithm;\r
+   *        secondly each of these bytes is encoded as "%xx".\r
+   * </ul>\r
+   *\r
+   * @param s The string to be encoded\r
+   * @return The encoded string\r
+   */\r
+  public static String encode(String s)\r
+  {\r
+    StringBuffer sbuf = new StringBuffer();\r
+    int len = s.length();\r
+    for (int i = 0; i < len; i++) {\r
+      int ch = s.charAt(i);\r
+      if ('A' <= ch && ch <= 'Z') {            // 'A'..'Z'\r
+        sbuf.append((char)ch);\r
+      } else if ('a' <= ch && ch <= 'z') {     // 'a'..'z'\r
+              sbuf.append((char)ch);\r
+      } else if ('0' <= ch && ch <= '9') {     // '0'..'9'\r
+              sbuf.append((char)ch);\r
+      } else if (ch == ' ') {                  // space\r
+              sbuf.append(hex[ch]);     //Note: changed from + to %20 for use according to http://www.doi.org/doi_handbook/2_Numbering.html#2.5.2.4\r
+      } else if (ch == '-' || ch == '_'                // unreserved\r
+          || ch == '.' || ch == '!'\r
+          || ch == '~' || ch == '*'\r
+          || ch == '\'' || ch == '('\r
+          || ch == ')') {\r
+        sbuf.append((char)ch);\r
+      } else if (ch <= 0x007f) {               // other ASCII\r
+              sbuf.append(hex[ch]);\r
+      } else if (ch <= 0x07FF) {               // non-ASCII <= 0x7FF\r
+              sbuf.append(hex[0xc0 | (ch >> 6)]);\r
+              sbuf.append(hex[0x80 | (ch & 0x3F)]);\r
+      } else {                                 // 0x7FF < ch <= 0xFFFF\r
+              sbuf.append(hex[0xe0 | (ch >> 12)]);\r
+              sbuf.append(hex[0x80 | ((ch >> 6) & 0x3F)]);\r
+              sbuf.append(hex[0x80 | (ch & 0x3F)]);\r
+      }\r
+    }\r
+    return sbuf.toString();\r
+  }\r
+  \r
+       /*\r
+        * Created: 17 April 1997\r
+        * Author: Bert Bos <bert@w3.org>\r
+        *\r
+        * unescape: http://www.w3.org/International/unescape.java\r
+        *\r
+        * Copyright © 1997 World Wide Web Consortium, (Massachusetts\r
+        * Institute of Technology, European Research Consortium for\r
+        * Informatics and Mathematics, Keio University). All Rights Reserved. \r
+        * This work is distributed under the W3C® Software License [1] in the\r
+        * hope that it will be useful, but WITHOUT ANY WARRANTY; without even\r
+        * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR\r
+        * PURPOSE.\r
+        *\r
+        * [1] http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231\r
+        */\r
+  public static String unescape(String s) {\r
+           StringBuffer sbuf = new StringBuffer () ;\r
+           int l  = s.length() ;\r
+           int ch = -1 ;\r
+           int b, sumb = 0;\r
+           for (int i = 0, more = -1 ; i < l ; i++) {\r
+             /* Get next byte b from URL segment s */\r
+             switch (ch = s.charAt(i)) {\r
+               case '%':\r
+                 ch = s.charAt (++i) ;\r
+                 int hb = (Character.isDigit ((char) ch) \r
+                           ? ch - '0'\r
+                           : 10+Character.toLowerCase((char) ch) - 'a') & 0xF ;\r
+                 ch = s.charAt (++i) ;\r
+                 int lb = (Character.isDigit ((char) ch)\r
+                           ? ch - '0'\r
+                           : 10+Character.toLowerCase ((char) ch)-'a') & 0xF ;\r
+                 b = (hb << 4) | lb ;\r
+                 break ;\r
+               case '+':\r
+                 b = ' ' ;\r
+                 break ;\r
+               default:\r
+                 b = ch ;\r
+             }\r
+             /* Decode byte b as UTF-8, sumb collects incomplete chars */\r
+             if ((b & 0xc0) == 0x80) {                 // 10xxxxxx (continuation byte)\r
+               sumb = (sumb << 6) | (b & 0x3f) ;       // Add 6 bits to sumb\r
+               if (--more == 0) sbuf.append((char) sumb) ; // Add char to sbuf\r
+             } else if ((b & 0x80) == 0x00) {          // 0xxxxxxx (yields 7 bits)\r
+               sbuf.append((char) b) ;                 // Store in sbuf\r
+             } else if ((b & 0xe0) == 0xc0) {          // 110xxxxx (yields 5 bits)\r
+               sumb = b & 0x1f;\r
+               more = 1;                               // Expect 1 more byte\r
+             } else if ((b & 0xf0) == 0xe0) {          // 1110xxxx (yields 4 bits)\r
+               sumb = b & 0x0f;\r
+               more = 2;                               // Expect 2 more bytes\r
+             } else if ((b & 0xf8) == 0xf0) {          // 11110xxx (yields 3 bits)\r
+               sumb = b & 0x07;\r
+               more = 3;                               // Expect 3 more bytes\r
+             } else if ((b & 0xfc) == 0xf8) {          // 111110xx (yields 2 bits)\r
+               sumb = b & 0x03;\r
+               more = 4;                               // Expect 4 more bytes\r
+             } else /*if ((b & 0xfe) == 0xfc)*/ {      // 1111110x (yields 1 bit)\r
+               sumb = b & 0x01;\r
+               more = 5;                               // Expect 5 more bytes\r
+             }\r
+             /* We don't test if the UTF-8 encoding is well-formed */\r
+           }\r
+           return sbuf.toString() ;\r
+         }\r
+\r
+}\r
diff --git a/cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/DoiTest.java b/cdmlib-commons/src/test/java/eu/etaxonomy/cdm/common/DoiTest.java
new file mode 100644 (file)
index 0000000..ff7bfef
--- /dev/null
@@ -0,0 +1,151 @@
+/**\r
+* Copyright (C) 2007 EDIT\r
+* European Distributed Institute of Taxonomy\r
+* http://www.e-taxonomy.eu\r
+*\r
+* The contents of this file are subject to the Mozilla Public License Version 1.1\r
+* See LICENSE.TXT at the top of this package for the full license terms.\r
+*/\r
+package eu.etaxonomy.cdm.common;\r
+\r
+import org.junit.Assert;\r
+import org.junit.Before;\r
+import org.junit.Test;\r
+\r
+/**\r
+ * \r
+ * Test class for testing the {@link DOI} class.\r
+ * \r
+ * For doi syntax see also http://www.doi.org/doi_handbook/2_Numbering.html\r
+ * or\r
+ * http://stackoverflow.com/questions/27910/finding-a-doi-in-a-document-or-page  \r
+ * \r
+ * @author a.mueller\r
+ *\r
+ */\r
+public class DoiTest {\r
+\r
+       /**\r
+        * @throws java.lang.Exception\r
+        */\r
+       @Before\r
+       public void setUp() throws Exception {\r
+       }\r
+\r
+       @Test\r
+       public void testValidParser() {\r
+               String validDoi = "10.1002/1234";\r
+               DOI doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1002", doi.getPrefix());\r
+               Assert.assertEquals("1234", doi.getSuffix());\r
+               \r
+               validDoi = "10.1002/(SICI)1522-2594(199911)42:5<952::AID-MRM16>3.0.CO;2-S";\r
+               doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1002", doi.getPrefix());\r
+               Assert.assertEquals("(SICI)1522-2594(199911)42:5<952::AID-MRM16>3.0.CO;2-S", doi.getSuffix());\r
+               \r
+               validDoi = "10.1007.10/978-3-642-28108-2_19";\r
+               doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1007.10", doi.getPrefix());\r
+               Assert.assertEquals("978-3-642-28108-2_19", doi.getSuffix());\r
+               \r
+               validDoi="10.1579/0044-7447(2006)35\\[89:RDUICP\\]2.0.CO;2";\r
+               doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1579", doi.getPrefix());\r
+               Assert.assertEquals("0044-7447(2006)35\\[89:RDUICP\\]2.0.CO;2", doi.getSuffix());\r
+\r
+       }\r
+       \r
+       @Test\r
+       public void testFromRegistrantCodeAndSuffix() {\r
+               DOI doi = DOI.fromRegistrantCodeAndSuffix("1579", "978-3-642-28108-2_19");\r
+               Assert.assertEquals("10.1579", doi.getPrefix());\r
+               Assert.assertEquals("978-3-642-28108-2_19", doi.getSuffix());\r
+               Assert.assertNotEquals("1234", doi.getSuffix());\r
+       }\r
+       \r
+       @Test\r
+       public void testParserFail() {\r
+               String invalidDoi = "10.4515260,51.1656910";  //must never match to avoid matches with geo coordinates\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "4210.1000/123456";  //directoryIndicator must always be 10\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "10.1002/12\u0004345";  //control characters (here U+0004) must fail\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "10.1a02/12345";  //registrant code must include only number and dots (to separate sub codes)\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "10.1002:12345";  //column separator is only allowed (+required) in URNs\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "10.1002/";  //doi must always have a suffix length > 0 (if this should changed in future, please do adapt equals and hashCode)\r
+               testInvalid(invalidDoi);\r
+               invalidDoi = "10./1234";  //doi must always have a registrant prefix length > 0 (if this should changed in future, please do adapt equals and hashCode)\r
+               testInvalid(invalidDoi);\r
+       }\r
+\r
+       @Test\r
+       public void testParserWithPrefixes() {\r
+               String validDoi = "DOI: 10.1002/1234";\r
+               DOI doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1002", doi.getPrefix());\r
+               Assert.assertEquals("1234", doi.getSuffix());\r
+               \r
+               validDoi = "http://doi.org/10.1002/1234";\r
+               doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.1002", doi.getPrefix());\r
+               Assert.assertEquals("1234", doi.getSuffix());\r
+       \r
+               \r
+               validDoi = "http://doi.org/urn:doi:10.123:456ABC%2Fzyz";\r
+               doi = DOI.fromString(validDoi);\r
+               Assert.assertEquals("10.123", doi.getPrefix());\r
+               Assert.assertEquals("456ABC/zyz", doi.getSuffix());  //urn must be percentage encoded ( / -> %2F)\r
+               \r
+       }\r
+       \r
+       @Test\r
+       public void testEquals() {\r
+               String validDoi = "10.1002/12a4";\r
+               DOI doi1 = DOI.fromString(validDoi);\r
+               validDoi = "10.1002/12A4";\r
+               DOI doi2 = DOI.fromString(validDoi);\r
+               Assert.assertEquals("DOIs must be equal case insensitive", doi1, doi2);\r
+               validDoi = "10.1002/12b4";\r
+               DOI doi3 = DOI.fromString(validDoi);\r
+               Assert.assertNotEquals("Different DOIs must not be equal", doi1, doi3);\r
+       }\r
+       \r
+       @Test\r
+       public void testAsURI() {\r
+               //mandatory encoding according to http://www.doi.org/doi_handbook/2_Numbering.html#2.5.2.4\r
+               String validDoi = "10.1002/1234%56\"78#90 12?34";\r
+               DOI doi1 = DOI.fromString(validDoi);\r
+               String uri = doi1.asURI();\r
+               Assert.assertEquals(DOI.HTTP_DOI_ORG + "10.1002/1234%2556%2278%2390%2012%3f34", uri);\r
+               \r
+               //recommendedEncoding\r
+               validDoi = "10.1002/1234<56>78{90}12^34";\r
+               doi1 = DOI.fromString(validDoi);\r
+               uri = doi1.asURI();\r
+               Assert.assertEquals(DOI.HTTP_DOI_ORG + "10.1002/1234%3c56%3e78%7b90%7d12%5e34", uri);\r
+               \r
+               //recommendedEncoding (cont.)\r
+               validDoi = "10.1002/1234[56]78`90|12\\34+56";\r
+               doi1 = DOI.fromString(validDoi);\r
+               uri = doi1.asURI();\r
+               Assert.assertEquals(DOI.HTTP_DOI_ORG + "10.1002/1234%5b56%5d78%6090%7c12%5c34%2b56", uri);\r
+               \r
+       }\r
+\r
+       \r
+\r
+       \r
+       private void testInvalid(String invalidDoi) {\r
+               try {\r
+                       DOI.fromString(invalidDoi);\r
+                       Assert.fail("DOI should not be parsable: " + invalidDoi);\r
+               } catch (IllegalArgumentException e) {\r
+                       //OK\r
+               }\r
+       }               \r
+\r
+}\r
diff --git a/cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/DOIUserType.java b/cdmlib-model/src/main/java/eu/etaxonomy/cdm/hibernate/DOIUserType.java
new file mode 100644 (file)
index 0000000..562b5fe
--- /dev/null
@@ -0,0 +1,112 @@
+/**\r
+* Copyright (C) 2007 EDIT\r
+* European Distributed Institute of Taxonomy \r
+* http://www.e-taxonomy.eu\r
+* \r
+* The contents of this file are subject to the Mozilla Public License Version 1.1\r
+* See LICENSE.TXT at the top of this package for the full license terms.\r
+*/\r
+\r
+package eu.etaxonomy.cdm.hibernate;\r
+\r
+import java.io.Serializable;\r
+import java.sql.PreparedStatement;\r
+import java.sql.ResultSet;\r
+import java.sql.SQLException;\r
+import java.sql.Types;\r
+\r
+import org.apache.log4j.Logger;\r
+import org.hibernate.HibernateException;\r
+import org.hibernate.engine.spi.SessionImplementor;\r
+import org.hibernate.type.StandardBasicTypes;\r
+import org.hibernate.usertype.UserType;\r
+import org.jadira.usertype.dateandtime.shared.spi.AbstractUserType;\r
+\r
+import eu.etaxonomy.cdm.common.DOI;\r
+\r
+/**\r
+ * Hibernate user type for the {@link DOI} class.\r
+ * @author a.mueller\r
+ * @created 05.09.2013\r
+ */\r
+public class DOIUserType  extends AbstractUserType implements UserType {\r
+       private static final long serialVersionUID = 2227841000128722278L;\r
+\r
+       @SuppressWarnings("unused")\r
+       private static final Logger logger = Logger.getLogger(DOIUserType.class);\r
+\r
+       private static final int[] SQL_TYPES = { Types.VARCHAR };\r
+\r
+       @Override\r
+       public Object deepCopy(Object o) throws HibernateException {\r
+               if (o == null) {\r
+            return null;\r
+        }\r
+               \r
+               DOI doi = (DOI) o;\r
+\r
+        try {\r
+                       return DOI.fromString(doi.toString());\r
+               } catch (IllegalArgumentException e) {\r
+                       throw new HibernateException(e);\r
+               }\r
+       }\r
+\r
+\r
+       @Override\r
+       public Serializable disassemble(Object value) throws HibernateException {\r
+               if(value == null) {\r
+                       return null;\r
+               } else {\r
+                   DOI doi = (DOI) value;\r
+                   return doi.toString();\r
+               }\r
+       }\r
+\r
+       @Override\r
+       public DOI nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) \r
+                       throws HibernateException, SQLException {\r
+        String val = (String) StandardBasicTypes.STRING.nullSafeGet(rs, names, session, owner);\r
+               \r
+               if(val == null) {\r
+                       return null;\r
+               } else {\r
+\r
+            try {\r
+                           return DOI.fromString(val);\r
+                   } catch (IllegalArgumentException e) {\r
+                           throw new HibernateException(e);\r
+                   }\r
+               }\r
+       }\r
+\r
+       @Override\r
+       public void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session) \r
+                       throws HibernateException, SQLException {\r
+               if (value == null) { \r
+            StandardBasicTypes.STRING.nullSafeSet(statement, value, index, session);\r
+        } else { \r
+               DOI doi = (DOI)value;\r
+            StandardBasicTypes.STRING.nullSafeSet(statement, doi.toString(), index, session);\r
+        }\r
+       }\r
+\r
+\r
+       /* (non-Javadoc)\r
+        * @see org.jadira.usertype.dateandtime.shared.spi.AbstractSingleColumnUserType#returnedClass()\r
+        */\r
+       @Override\r
+       public Class returnedClass() {\r
+               return DOI.class;\r
+       }\r
+\r
+       @Override\r
+       public int[] sqlTypes() {\r
+               return SQL_TYPES;\r
+       }\r
+\r
+       \r
+\r
+\r
+\r
+}\r
index 55bb11b62d6de30861f572b1c7ead8455206c465..1d20c3b5784185d2f9b634f4e97e5a6ce543a658 100644 (file)
        @org.hibernate.annotations.TypeDef(name="partialUserType", typeClass=eu.etaxonomy.cdm.hibernate.PartialUserType.class),\r
        @org.hibernate.annotations.TypeDef(name="uuidUserType", typeClass=eu.etaxonomy.cdm.hibernate.UUIDUserType.class),\r
        @org.hibernate.annotations.TypeDef(name="uriUserType", typeClass=eu.etaxonomy.cdm.hibernate.URIUserType.class),\r
-       @org.hibernate.annotations.TypeDef(name="enumUserType", typeClass=eu.etaxonomy.cdm.hibernate.EnumUserType.class)        \r
+       @org.hibernate.annotations.TypeDef(name="enumUserType", typeClass=eu.etaxonomy.cdm.hibernate.EnumUserType.class),       \r
+       @org.hibernate.annotations.TypeDef(name="doiUserType", typeClass=eu.etaxonomy.cdm.hibernate.DOIUserType.class)  \r
 })\r
 @org.hibernate.annotations.AnyMetaDef(name = "CdmBase" ,\r
                                              metaType="string",\r
index 763f363ced3d745070c148b5080d1f9f93fe2ab9..7523eeb2e3813fd302faf15468a0b040a445a5d9 100644 (file)
@@ -9,6 +9,8 @@
 \r
 package eu.etaxonomy.cdm.model.reference;\r
 \r
+import eu.etaxonomy.cdm.common.DOI;\r
+\r
 /**\r
  * This base interface represents all different kind of published \r
  * {@link IReference references} which constitute a physical \r
@@ -54,8 +56,8 @@ public interface IPublicationBase extends IReference {
        /**\r
         * @return\r
         */\r
-       public String getDoi();\r
+       public DOI getDoi();\r
 \r
-       public void setDoi(String doi);\r
+       public void setDoi(DOI doi);\r
        \r
 }\r
index 69e3bdb0aede5ccd4e9a56139e01a60fbc99e688..b0f13a6b82d9894221241985229fc7034f85e5cb 100644 (file)
@@ -45,6 +45,7 @@ import org.hibernate.search.annotations.Field;
 import org.hibernate.search.annotations.IndexedEmbedded;\r
 import org.hibernate.validator.constraints.Length;\r
 \r
+import eu.etaxonomy.cdm.common.DOI;\r
 import eu.etaxonomy.cdm.model.agent.Institution;\r
 import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;\r
 import eu.etaxonomy.cdm.model.common.TimePeriod;\r
@@ -207,10 +208,10 @@ public class Reference<S extends IReferenceBaseCacheStrategy> extends Identifiab
 \r
     @XmlElement(name = "Doi")\r
     @Field\r
-    @NullOrNotEmpty\r
-       @Length(max = 255)\r
-//     @Pattern(regexp = "(?=.{13}$)\\d{1,5}([- ])\\d{1,7}\\1\\d{1,6}\\1(\\d|X)$", groups = Level2.class, message = "{eu.etaxonomy.cdm.model.reference.Reference.doi.message}")\r
-       protected String doi;\r
+//    @NullOrNotEmpty\r
+//     @Length(max = 1000)\r
+    @Type(type="doiUserType")\r
+    protected DOI doi;\r
 \r
 \r
        @XmlElement(name = "ISSN")\r
@@ -487,12 +488,12 @@ public class Reference<S extends IReferenceBaseCacheStrategy> extends Identifiab
        }\r
        \r
     @Override\r
-       public String getDoi() {\r
+       public DOI getDoi() {\r
                return doi;\r
        }\r
 \r
     @Override\r
-       public void setDoi(String doi) {\r
+       public void setDoi(DOI doi) {\r
                this.doi = doi;\r
        }\r
 \r
index df046470d3b0c2559ff7488799c4802c6bb89d1c..0016c7e7e4f0e0619d4afb2d1dfb7ed78b824e11 100644 (file)
@@ -28,6 +28,7 @@ import java.util.UUID;
 import org.apache.log4j.Logger;\r
 \r
 import eu.etaxonomy.cdm.common.CdmUtils;\r
+import eu.etaxonomy.cdm.common.DOI;\r
 import eu.etaxonomy.cdm.common.DoubleResult;\r
 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;\r
 import eu.etaxonomy.cdm.model.common.CdmBase;\r
@@ -211,6 +212,8 @@ public class DefaultMatchStrategy extends StrategyBase implements IMatchStrategy
                                        //result &= matchPrimitiveField(matchFirst, matchSecond, fieldMatcher, replaceModeList);\r
                                }else if(fieldType == URI.class){\r
                                        result &= matchPrimitiveField(matchFirst, matchSecond, fieldMatcher, replaceModeList);\r
+                               }else if(fieldType == DOI.class){\r
+                                       result &= matchPrimitiveField(matchFirst, matchSecond, fieldMatcher, replaceModeList);\r
                                }else if(isSingleCdmBaseObject(fieldType)){\r
                                        result &= matchPrimitiveField(matchFirst, matchSecond, fieldMatcher, replaceModeList);\r
                                }else if (isCollection(fieldType)){\r