View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.datamodel.metadata;
20  
21  import java.text.NumberFormat;
22  import java.util.ArrayList;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Objects;
28  import java.util.stream.Collectors;
29  
30  import org.apache.logging.log4j.LogManager;
31  import org.apache.logging.log4j.Logger;
32  import org.mycore.common.MCRException;
33  import org.mycore.common.MCRUtils;
34  import org.mycore.common.config.MCRConfiguration2;
35  import org.mycore.datamodel.common.MCRXMLMetadataManager;
36  
37  import com.fasterxml.jackson.annotation.JsonClassDescription;
38  import com.fasterxml.jackson.annotation.JsonCreator;
39  import com.fasterxml.jackson.annotation.JsonFormat;
40  import com.fasterxml.jackson.annotation.JsonValue;
41  
42  /**
43   * This class holds all informations and methods to handle the MyCoRe Object ID.
44   * The MyCoRe Object ID is a special ID to identify each metadata object with
45   * three parts, they are the project identifier, the type identifier and a
46   * string with a number. The syntax of the ID is "<em>projectID</em>_
47   * <em>typeID</em>_ <em>number</em>" as "<em>String_String_Integer</em>".
48   *
49   * @author Jens Kupferschmidt
50   * @author Thomas Scheffler (yagee)
51   * @version $Revision$ $Date$
52   */
53  @JsonClassDescription("MyCoRe ObjectID in form {project}_{type}_{int32}, "
54      + "where project is a namespace and type defines the datamodel")
55  @JsonFormat(shape = JsonFormat.Shape.STRING)
56  public final class MCRObjectID implements Comparable<MCRObjectID> {
57      /**
58       * public constant value for the MCRObjectID length
59       */
60      public static final int MAX_LENGTH = 64;
61  
62      private static final MCRObjectIDFormat ID_FORMAT = new MCRObjectIDDefaultFormat();
63  
64      private static final Logger LOGGER = LogManager.getLogger(MCRObjectID.class);
65  
66      // counter for the next IDs per project base ID
67      private static HashMap<String, Integer> lastNumber = new HashMap<>();
68  
69      private static HashSet<String> VALID_TYPE_LIST;
70  
71      static {
72          final String confPrefix = "MCR.Metadata.Type.";
73          VALID_TYPE_LIST = MCRConfiguration2.getPropertiesMap()
74              .entrySet()
75              .stream()
76              .filter(p -> p.getKey().startsWith(confPrefix))
77              .filter(p -> Boolean.parseBoolean(p.getValue()))
78              .map(prop -> prop.getKey().substring(confPrefix.length()))
79              .collect(Collectors.toCollection(HashSet::new));
80      }
81  
82      // data of the ID
83      private String projectId, objectType, combinedId;
84  
85      private int numberPart;
86  
87      /**
88       * The constructor for MCRObjectID from a given string.
89       *
90       * @exception MCRException
91       *                if the given string is not valid.
92       */
93      MCRObjectID(String id) throws MCRException {
94          if (!setID(id)) {
95              throw new MCRException("The ID is not valid: " + id
96                  + " , it should match the pattern String_String_Integer");
97          }
98      }
99  
100     /**
101      * Returns a MCRObjectID from a given base ID string. A base ID is
102      * <em>project_id</em>_<em>type_id</em>. The number is computed by this
103      * method. It is the next free number of an item in the database for the
104      * given project ID and type ID, with the following additional restriction:
105      * The ID returned can be divided by idFormat.numberDistance without remainder.
106      * The ID returned minus the last ID returned is at least idFormat.numberDistance.
107      *
108      * Example for number distance of 1 (default):
109      *   last ID = 7, next ID = 8
110      *   last ID = 8, next ID = 9
111      *
112      * Example for number distance of 2:
113      *   last ID = 7, next ID = 10
114      *   last ID = 8, next ID = 10
115      *   last ID = 10, next ID = 20
116      *
117      * @param baseId
118      *            <em>project_id</em>_<em>type_id</em>
119      */
120     public static synchronized MCRObjectID getNextFreeId(String baseId) {
121         return getNextFreeId(baseId, 0);
122     }
123 
124     /**
125      * Returns a MCRObjectID from a given the components of a base ID string. A base ID is
126      * <em>project_id</em>_<em>type_id</em>. The number is computed by this
127      * method. It is the next free number of an item in the database for the
128      * given project ID and type ID, with the following additional restriction:
129      * The ID returned can be divided by idFormat.numberDistance without remainder.
130      * The ID returned minus the last ID returned is at least idFormat.numberDistance.
131      *
132      * Example for number distance of 1 (default):
133      *   last ID = 7, next ID = 8
134      *   last ID = 8, next ID = 9
135      *
136      * Example for number distance of 2:
137      *   last ID = 7, next ID = 10
138      *   last ID = 8, next ID = 10
139      *   last ID = 10, next ID = 20
140      *
141      * @param projectId
142      *            The first component of <em>project_id</em>_<em>type_id</em>
143      * @param type
144      *            The second component of <em>project_id</em>_<em>type_id</em>
145      */
146     public static synchronized MCRObjectID getNextFreeId(String projectId, String type) {
147         return getNextFreeId(projectId + "_" + type);
148     }
149 
150     /**
151      * Returns a MCRObjectID from a given base ID string. Same as
152      * {@link #getNextFreeId(String)} but the additional parameter acts as a
153      * lower limit for integer part of the ID.
154      *
155      * @param baseId
156      *            <em>project_id</em>_<em>type_id</em>
157      * @param maxInWorkflow
158      *            returned integer part of id will be at least
159      *            <code>maxInWorkflow + 1</code>
160      */
161     public static synchronized MCRObjectID getNextFreeId(String baseId, int maxInWorkflow) {
162         int last = Math.max(getLastIDNumber(baseId), maxInWorkflow);
163         int numberDistance = ID_FORMAT.numberDistance();
164         int next = last + numberDistance;
165 
166         int rest = next % numberDistance;
167         if (rest != 0) {
168             next += numberDistance - rest;
169         }
170 
171         lastNumber.put(baseId, next);
172         String[] idParts = getIDParts(baseId);
173         return getInstance(formatID(idParts[0], idParts[1], next));
174     }
175 
176     /**
177      * Returns the last ID number used or reserved for the given object base
178      * type. This may return the value 0 when there is no ID last used or in the
179      * store.
180      */
181     private static int getLastIDNumber(String baseId) {
182         int lastIDKnown = lastNumber.getOrDefault(baseId, 0);
183 
184         String[] idParts = getIDParts(baseId);
185         int highestStoredID = MCRXMLMetadataManager.instance().getHighestStoredID(idParts[0], idParts[1]);
186 
187         return Math.max(lastIDKnown, highestStoredID);
188     }
189 
190     /**
191      * Returns the last ID used or reserved for the given object base type.
192      *
193      * @return a valid MCRObjectID, or null when there is no ID for the given
194      *         type
195      */
196     public static MCRObjectID getLastID(String baseId) {
197         int lastIDNumber = getLastIDNumber(baseId);
198         if (lastIDNumber == 0) {
199             return null;
200         }
201 
202         String[] idParts = getIDParts(baseId);
203         return getInstance(formatID(idParts[0], idParts[1], lastIDNumber));
204     }
205 
206     /**
207      * This method instantiate this class with a given identifier in MyCoRe schema.
208      *
209      * @param id
210      *          the MCRObjectID
211      * @return an MCRObjectID class instance
212      * @exception MCRException if the given identifier is not valid
213      */
214     @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
215     public static MCRObjectID getInstance(String id) {
216         return MCRObjectIDPool.getMCRObjectID(Objects.requireNonNull(id, "'id' must not be null."));
217     }
218 
219     /**
220      * Normalizes to a object ID of form <em>project_id</em>_ <em>type_id</em>_
221      * <em>number</em>, where number has leading zeros.
222      * @return <em>project_id</em>_<em>type_id</em>_<em>number</em>
223      */
224     public static String formatID(String projectID, String type, int number) {
225         if (projectID == null) {
226             throw new IllegalArgumentException("projectID cannot be null");
227         }
228         if (type == null) {
229             throw new IllegalArgumentException("type cannot be null");
230         }
231         if (number < 0) {
232             throw new IllegalArgumentException("number must be non negative integer");
233         }
234         return projectID + '_' + type.toLowerCase(Locale.ROOT) + '_' + ID_FORMAT.numberFormat().format(number);
235     }
236 
237     /**
238      * Normalizes to a object ID of form <em>project_id</em>_ <em>type_id</em>_
239      * <em>number</em>, where number has leading zeros.
240      *
241      * @param baseID
242      *            is <em>project_id</em>_<em>type_id</em>
243      * @return <em>project_id</em>_<em>type_id</em>_<em>number</em>
244      */
245     public static String formatID(String baseID, int number) {
246         String[] idParts = getIDParts(baseID);
247         return formatID(idParts[0], idParts[1], number);
248     }
249 
250     /**
251      * Splits the submitted <code>id</code> in its parts.
252      * <code>MyCoRe_document_00000001</code> would be transformed in { "MyCoRe",
253      * "document", "00000001" }
254      *
255      * @param id
256      *            either baseID or complete ID
257      */
258     public static String[] getIDParts(String id) {
259         return id.split("_");
260     }
261 
262     /**
263      * Returns a list of available mycore object types.
264      */
265     public static List<String> listTypes() {
266         return new ArrayList<>(VALID_TYPE_LIST);
267     }
268 
269     /**
270      * Check whether the type passed is a valid type in the current mycore environment.
271      * That being said property <code>MCR.Metadata.Type.&#60;type&#62;</code> must be set to <code>true</code> in mycore.properties.
272      *
273      * @param type the type to check
274      * @return true if valid, false otherwise
275      */
276     public static boolean isValidType(String type) {
277         return VALID_TYPE_LIST.contains(type);
278     }
279 
280     /**
281      * Checks if the given id is a valid mycore id in the form of {project}_{object_type}_{number}.
282      *
283      * @param id the id to check
284      * @return true if the id is valid, false otherwise
285      */
286     public static boolean isValid(String id) {
287         if (id == null) {
288             return false;
289         }
290         String mcrId = id.trim();
291         if (mcrId.length() > MAX_LENGTH) {
292             return false;
293         }
294         String[] idParts = getIDParts(mcrId);
295         if (idParts.length != 3) {
296             return false;
297         }
298         String objectType = idParts[1].toLowerCase(Locale.ROOT).intern();
299         if (!MCRConfiguration2.getBoolean("MCR.Metadata.Type." + objectType).orElse(false)) {
300             LOGGER.warn("Property MCR.Metadata.Type.{} is not set. Thus {} cannot be a valid id", objectType, id);
301             return false;
302         }
303         try {
304             Integer numberPart = Integer.parseInt(idParts[2]);
305             if (numberPart < 0) {
306                 return false;
307             }
308         } catch (NumberFormatException e) {
309             return false;
310         }
311         return true;
312     }
313 
314     /**
315      * This method get the string with <em>project_id</em>. If the ID is not
316      * valid, an empty string was returned.
317      *
318      * @return the string of the project id
319      */
320     public String getProjectId() {
321         return projectId;
322     }
323 
324     /**
325      * This method gets the string with <em>type_id</em>. If the ID is not
326      * valid, an empty string will be returned.
327      *
328      * @return the string of the type id
329      */
330     public String getTypeId() {
331         return objectType;
332     }
333 
334     /**
335      * This method gets the string with <em>number</em>. If the ID is not valid,
336      * an empty string will be returned.
337      *
338      * @return the string of the number
339      */
340     public String getNumberAsString() {
341         return ID_FORMAT.numberFormat().format(numberPart);
342     }
343 
344     /**
345      * This method gets the integer with <em>number</em>. If the ID is not
346      * valid, -1 will be returned.
347      *
348      * @return the number as integer
349      */
350     public int getNumberAsInteger() {
351         return numberPart;
352     }
353 
354     /**
355      * This method gets the basic string with <em>project_id</em>_
356      * <em>type_id</em>. If the Id is not valid, an empty string will be
357      * returned.
358      *
359      * @return the string of the schema name
360      */
361     public String getBase() {
362         return projectId + "_" + objectType;
363     }
364 
365     /**
366      * This method return the validation value of a MCRObjectId and store the
367      * components in this class. The <em>type_id</em> was set to lower case. The
368      * MCRObjectID is valid if:
369      * <ul>
370      * <li>The argument is not null.
371      * <li>The syntax of the ID is <em>project_id</em>_<em>type_id</em>_
372      * <em>number</em> as <em>String_String_Integer</em>.
373      * <li>The ID is not longer as MAX_LENGTH.
374      * <li>The ID has only characters, they must not encoded.
375      * </ul>
376      *
377      * @param id
378      *            the MCRObjectID
379      * @return the validation value, true if the MCRObjectID is correct,
380      *         otherwise return false
381      */
382     private boolean setID(String id) {
383         if (!isValid(id)) {
384             return false;
385         }
386         String[] idParts = getIDParts(id.trim());
387         projectId = idParts[0].intern();
388         objectType = idParts[1].toLowerCase(Locale.ROOT).intern();
389         numberPart = Integer.parseInt(idParts[2]);
390         this.combinedId = formatID(projectId, objectType, numberPart);
391         return true;
392     }
393 
394     /**
395      * This method check this data again the input and retuns the result as
396      * boolean.
397      *
398      * @param in
399      *            the MCRObjectID to check
400      * @return true if all parts are equal, else return false
401      */
402     public boolean equals(MCRObjectID in) {
403         return this == in || (in != null && toString().equals(in.toString()));
404     }
405 
406     /**
407      * This method check this data again the input and retuns the result as
408      * boolean.
409      *
410      * @param in
411      *            the MCRObjectID to check
412      * @return true if all parts are equal, else return false.
413      * @see java.lang.Object#equals(Object)
414      */
415     @Override
416     public boolean equals(Object in) {
417         if (in instanceof MCRObjectID) {
418             return equals((MCRObjectID) in);
419         }
420         return false;
421     }
422 
423     @Override
424     public int compareTo(MCRObjectID o) {
425         return MCRUtils.compareParts(this, o,
426             MCRObjectID::getProjectId,
427             MCRObjectID::getTypeId,
428             MCRObjectID::getNumberAsInteger);
429     }
430 
431     /**
432      * @see java.lang.Object#toString()
433      * @return {@link #formatID(String, String, int)} with
434      *         {@link #getProjectId()}, {@link #getTypeId()},
435      *         {@link #getNumberAsInteger()}
436      */
437     @Override
438     @JsonValue
439     public String toString() {
440         return combinedId;
441     }
442 
443     /**
444      * returns toString().hashCode()
445      *
446      * @see #toString()
447      * @see java.lang.Object#hashCode()
448      */
449     @Override
450     public int hashCode() {
451         return toString().hashCode();
452     }
453 
454     public interface MCRObjectIDFormat {
455         int numberDistance();
456 
457         NumberFormat numberFormat();
458     }
459 
460     private static class MCRObjectIDDefaultFormat implements MCRObjectIDFormat {
461 
462         private int numberDistance;
463 
464         /**
465          * First invocation may return MCR.Metadata.ObjectID.InitialNumberDistance if set,
466          * following invocations will return MCR.Metadata.ObjectID.NumberDistance.
467          * The default for both is 1.
468          */
469         @Override
470         public int numberDistance() {
471             if (numberDistance == 0) {
472                 numberDistance = MCRConfiguration2.getInt("MCR.Metadata.ObjectID.NumberDistance").orElse(1);
473                 return MCRConfiguration2.getInt("MCR.Metadata.ObjectID.InitialNumberDistance").orElse(numberDistance);
474             }
475             return numberDistance;
476         }
477 
478         @Override
479         public NumberFormat numberFormat() {
480             String numberPattern = MCRConfiguration2.getString("MCR.Metadata.ObjectID.NumberPattern")
481                 .orElse("0000000000").trim();
482             NumberFormat format = NumberFormat.getIntegerInstance(Locale.ROOT);
483             format.setGroupingUsed(false);
484             format.setMinimumIntegerDigits(numberPattern.length());
485             return format;
486         }
487 
488     }
489 
490 }