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.pi.urn.rest;
20  
21  import java.io.IOException;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.text.MessageFormat;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Date;
28  import java.util.Iterator;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Optional;
33  import java.util.function.BiConsumer;
34  import java.util.function.Function;
35  import java.util.function.Predicate;
36  import java.util.function.Supplier;
37  import java.util.regex.Pattern;
38  import java.util.stream.Collectors;
39  import java.util.stream.Stream;
40  
41  import org.apache.logging.log4j.LogManager;
42  import org.apache.logging.log4j.Logger;
43  import org.mycore.access.MCRAccessException;
44  import org.mycore.backend.jpa.MCREntityManagerProvider;
45  import org.mycore.common.MCRException;
46  import org.mycore.common.MCRPersistenceException;
47  import org.mycore.datamodel.common.MCRActiveLinkException;
48  import org.mycore.datamodel.metadata.MCRBase;
49  import org.mycore.datamodel.metadata.MCRDerivate;
50  import org.mycore.datamodel.metadata.MCRMetadataManager;
51  import org.mycore.datamodel.metadata.MCRObjectDerivate;
52  import org.mycore.datamodel.metadata.MCRObjectID;
53  import org.mycore.datamodel.metadata.MCRObjectService;
54  import org.mycore.datamodel.niofs.MCRPath;
55  import org.mycore.pi.MCRPIManager;
56  import org.mycore.pi.MCRPIService;
57  import org.mycore.pi.backend.MCRPI;
58  import org.mycore.pi.exceptions.MCRPersistentIdentifierException;
59  import org.mycore.pi.urn.MCRDNBURN;
60  import org.mycore.pi.urn.MCRDNBURNParser;
61  
62  import com.google.gson.Gson;
63  
64  import jakarta.persistence.EntityTransaction;
65  
66  /**
67   * Service for assigning granular URNs to Derivate. You can call it with a Derivate-ID and it will assign a Base-URN for
68   * the Derivate and granular URNs for every file in the Derivate (except IgnoreFileNames). If you then add a file to
69   * Derivate you can call with Derivate-ID and additional path of the file. E.g. mir_derivate_00000060 and /image1.jpg
70   * <p>
71   * <b>Inscriber is ignored with this {@link MCRPIService}</b>
72   * </p>
73   * Configuration Parameter(s): <dl>
74   * <dt>IgnoreFileNames</dt>
75   * <dd>Comma seperated list of regex file which should not have a urn assigned. Default: mets\\.xml</dd> </dl>
76   */
77  public class MCRURNGranularRESTService extends MCRPIService<MCRDNBURN> {
78  
79      private static final Logger LOGGER = LogManager.getLogger();
80  
81      private final Function<MCRDerivate, Stream<MCRPath>> derivateFileStream;
82  
83      public MCRURNGranularRESTService() {
84          this(MCRURNGranularRESTService::defaultDerivateFileStream);
85      }
86  
87      public MCRURNGranularRESTService(Function<MCRDerivate, Stream<MCRPath>> derivateFileStreamFunc) {
88          super(MCRDNBURN.TYPE);
89          this.derivateFileStream = derivateFileStreamFunc;
90      }
91  
92      private static Stream<MCRPath> defaultDerivateFileStream(MCRDerivate derivate) {
93          MCRObjectID derivateId = derivate.getId();
94          Path derivRoot = MCRPath.getPath(derivateId.toString(), "/");
95  
96          try {
97              return Files.walk(derivRoot)
98                  .map(MCRPath::toMCRPath)
99                  .filter(p -> !Files.isDirectory(p))
100                 .filter(p -> !p.equals(derivRoot));
101         } catch (IOException e) {
102             LOGGER.error("I/O error while access the starting file of derivate {}!", derivateId, e);
103         } catch (SecurityException s) {
104             LOGGER.error("No access to starting file of derivate {}!", derivateId, s);
105         }
106 
107         return Stream.empty();
108     }
109 
110     @Override
111     public MCRDNBURN register(MCRBase obj, String filePath, boolean updateObject)
112         throws MCRAccessException, MCRActiveLinkException, MCRPersistentIdentifierException {
113         this.validateRegistration(obj, filePath);
114 
115         if (obj instanceof MCRDerivate) {
116             MCRDerivate derivate = (MCRDerivate) obj;
117             return registerURN(derivate, filePath);
118         } else {
119             throw new MCRPersistentIdentifierException("Object " + obj.getId() + " is not a MCRDerivate!");
120         }
121     }
122 
123     private MCRDNBURN registerURN(MCRDerivate deriv, String filePath) throws MCRPersistentIdentifierException {
124         MCRObjectID derivID = deriv.getId();
125 
126         Function<String, Integer> countCreatedPI = s -> MCRPIManager
127             .getInstance()
128             .getCreatedIdentifiers(derivID, getType(), getServiceID())
129             .size();
130 
131         int seed = Optional.of(filePath)
132             .filter(p -> !"".equals(p))
133             .map(countCreatedPI)
134             .map(count -> count + 1)
135             .orElse(1);
136 
137         MCRDNBURN derivURN = Optional
138             .ofNullable(deriv.getDerivate())
139             .map(MCRObjectDerivate::getURN)
140             .flatMap(new MCRDNBURNParser()::parse)
141             .orElseGet(() -> createNewURN(deriv));
142 
143         String setID = derivID.getNumberAsString();
144         GranularURNGenerator granularURNGen = new GranularURNGenerator(seed, derivURN, setID);
145         Function<MCRPath, Supplier<String>> generateURN = p -> granularURNGen.getURNSupplier();
146 
147         LinkedHashMap<Supplier<String>, MCRPath> urnPathMap = derivateFileStream.apply(deriv)
148             .filter(notInIgnoreList().and(matchFile(filePath)))
149             .sorted()
150             .collect(Collectors.toMap(generateURN, p -> p, (m1, m2) -> m1,
151                 LinkedHashMap::new));
152 
153         if (!"".equals(filePath) && urnPathMap.isEmpty()) {
154             String errMsg = new MessageFormat("File {0} does not exist in {1}.\n", Locale.ROOT)
155                 .format(new Object[] { filePath, derivID.toString() })
156                 + "Use absolute path of file without owner ID like /abs/path/to/file.\n";
157 
158             throw new MCRPersistentIdentifierException(errMsg);
159         }
160 
161         urnPathMap.forEach(createFileMetadata(deriv).andThen(persistURN(deriv)));
162 
163         try {
164             MCRMetadataManager.update(deriv);
165         } catch (MCRPersistenceException | MCRAccessException e) {
166             LOGGER.error("Error while updating derivate {}", derivID, e);
167         }
168 
169         EntityTransaction transaction = MCREntityManagerProvider
170             .getCurrentEntityManager()
171             .getTransaction();
172 
173         if (!transaction.isActive()) {
174             transaction.begin();
175         }
176 
177         transaction.commit();
178 
179         return derivURN;
180     }
181 
182     public MCRDNBURN createNewURN(MCRDerivate deriv) {
183         MCRObjectID derivID = deriv.getId();
184 
185         try {
186             MCRDNBURN derivURN = getNewIdentifier(deriv, "");
187             deriv.getDerivate().setURN(derivURN.asString());
188 
189             persistURNStr(deriv, null).accept(derivURN::asString, "");
190 
191             if (Boolean.valueOf(getProperties().getOrDefault("supportDfgViewerURN", "false"))) {
192                 String suffix = "dfg";
193                 persistURNStr(deriv, null, getServiceID() + "-" + suffix)
194                     .accept(() -> derivURN.withNamespaceSuffix(suffix + "-").asString(), "");
195             }
196 
197             return derivURN;
198         } catch (MCRPersistentIdentifierException e) {
199             throw new MCRPICreationException("Could not create new URN for " + derivID, e);
200         }
201     }
202 
203     private BiConsumer<Supplier<String>, MCRPath> createFileMetadata(MCRDerivate deriv) {
204         return (urnSup, path) -> deriv.getDerivate().getOrCreateFileMetadata(path, urnSup.get());
205     }
206 
207     private BiConsumer<Supplier<String>, MCRPath> persistURN(MCRDerivate deriv) {
208         return (urnSup, path) -> persistURNStr(deriv, null).accept(urnSup, path.getOwnerRelativePath());
209     }
210 
211     private BiConsumer<Supplier<String>, String> persistURNStr(MCRDerivate deriv, Date registerDate) {
212         return (urnSup, path) -> persistURNStr(deriv, registerDate, getServiceID()).accept(urnSup, path);
213     }
214 
215     private BiConsumer<Supplier<String>, String> persistURNStr(MCRDerivate deriv, Date registerDate, String serviceID) {
216         return (urnSup, path) -> {
217             MCRPI mcrpi = new MCRPI(urnSup.get(), getType(), deriv.getId().toString(), path, serviceID,
218                 registerDate);
219             MCREntityManagerProvider.getCurrentEntityManager().persist(mcrpi);
220         };
221     }
222 
223     private Predicate<MCRPath> matchFile(String ownerRelativPath) {
224         return path -> Optional.of(ownerRelativPath)
225             .filter(""::equals)
226             .map(p -> Boolean.TRUE)
227             .orElseGet(() -> path.getOwnerRelativePath().equals(ownerRelativPath));
228 
229     }
230 
231     private Predicate<MCRPath> notInIgnoreList() {
232         Supplier<? extends RuntimeException> errorInIgnorList = () -> new RuntimeException(
233             "Error in ignore filename list!");
234 
235         return path -> getIgnoreFileList()
236             .stream()
237             .map(Pattern::compile)
238             .map(Pattern::asPredicate)
239             .map(Predicate::negate)
240             .reduce(Predicate::and)
241             .orElseThrow(errorInIgnorList)
242             .test(path.getOwnerRelativePath());
243     }
244 
245     private List<String> getIgnoreFileList() {
246         List<String> ignoreFileNamesList = new ArrayList<>();
247         String ignoreFileNames = getProperties().get("IgnoreFileNames");
248         if (ignoreFileNames != null) {
249             ignoreFileNamesList.addAll(Arrays.asList(ignoreFileNames.split(",")));
250         } else {
251             ignoreFileNamesList.add("mets\\.xml"); // default value
252         }
253         return ignoreFileNamesList;
254     }
255 
256     @Override
257     protected void registerIdentifier(MCRBase obj, String additional, MCRDNBURN urn)
258         throws MCRPersistentIdentifierException {
259         // not used in this impl
260     }
261 
262     @Override
263     protected void delete(MCRDNBURN identifier, MCRBase obj, String additional)
264         throws MCRPersistentIdentifierException {
265         throw new MCRPersistentIdentifierException("Delete is not supported for " + getType());
266     }
267 
268     @Override
269     protected void update(MCRDNBURN identifier, MCRBase obj, String additional)
270         throws MCRPersistentIdentifierException {
271         //TODO: improve API, don't override method to do nothing
272         LOGGER.info("No update in this implementation");
273     }
274 
275     @Override
276     public void updateFlag(MCRObjectID id, String additional, MCRPI mcrpi) {
277         MCRBase obj = MCRMetadataManager.retrieve(id);
278         MCRObjectService service = obj.getService();
279         ArrayList<String> flags = service.getFlags(MCRPIService.PI_FLAG);
280         Gson gson = getGson();
281 
282         //just update flag for derivate, where additional is ""
283         if ("".equals(additional)) {
284             Iterator<String> flagsIter = flags.iterator();
285             while (flagsIter.hasNext()) {
286                 String flagStr = flagsIter.next();
287                 MCRPI currentPi = gson.fromJson(flagStr, MCRPI.class);
288 
289                 if ("".equals(currentPi.getAdditional())
290                     && currentPi.getIdentifier().equals(mcrpi.getIdentifier())) {
291                     //remove flag for update
292                     flagsIter.remove();
293                 }
294             }
295 
296             addFlagToObject(obj, mcrpi);
297             try {
298                 MCRMetadataManager.update(obj);
299             } catch (Exception e) {
300                 throw new MCRException("Could not update flags of object " + id, e);
301             }
302         }
303     }
304 
305     private static class GranularURNGenerator {
306         private final MCRDNBURN urn;
307 
308         private final String setID;
309 
310         private int counter;
311 
312         private String leadingZeros;
313 
314         GranularURNGenerator(int seed, MCRDNBURN derivURN, String setID) {
315             this.counter = seed;
316             this.urn = derivURN;
317             this.setID = setID;
318         }
319 
320         public Supplier<String> getURNSupplier() {
321             int currentCount = counter++;
322             return () -> urn.toGranular(setID, getIndex(currentCount)).asString();
323         }
324 
325         public String getIndex(int currentCount) {
326             return String.format(Locale.getDefault(), leadingZeros(counter), currentCount);
327         }
328 
329         private String leadingZeros(int i) {
330             if (leadingZeros == null) {
331                 leadingZeros = "%0" + numDigits(i) + "d";
332             }
333 
334             return leadingZeros;
335         }
336 
337         private long numDigits(long n) {
338             if (n < 10) {
339                 return 1;
340             }
341             return 1 + numDigits(n / 10);
342         }
343     }
344 
345     public class MCRPICreationException extends RuntimeException {
346         public MCRPICreationException(String message, Throwable cause) {
347             super(message, cause);
348         }
349     }
350 }