1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.datamodel.common;
20
21 import java.io.File;
22 import java.io.FileNotFoundException;
23 import java.io.IOException;
24 import java.net.URI;
25 import java.net.URISyntaxException;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.text.MessageFormat;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.Date;
34 import java.util.HashSet;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Optional;
39 import java.util.concurrent.TimeUnit;
40 import java.util.stream.Collectors;
41 import java.util.stream.Stream;
42
43 import org.apache.logging.log4j.LogManager;
44 import org.apache.logging.log4j.Logger;
45 import org.jdom2.Document;
46 import org.jdom2.JDOMException;
47 import org.mycore.common.MCRCache;
48 import org.mycore.common.MCRPersistenceException;
49 import org.mycore.common.config.MCRConfiguration2;
50 import org.mycore.common.config.MCRConfigurationBase;
51 import org.mycore.common.config.MCRConfigurationException;
52 import org.mycore.common.content.MCRContent;
53 import org.mycore.common.content.MCRJDOMContent;
54 import org.mycore.datamodel.ifs2.MCRMetadataStore;
55 import org.mycore.datamodel.ifs2.MCRMetadataVersion;
56 import org.mycore.datamodel.ifs2.MCRObjectIDFileSystemDate;
57 import org.mycore.datamodel.ifs2.MCRStore;
58 import org.mycore.datamodel.ifs2.MCRStoreCenter;
59 import org.mycore.datamodel.ifs2.MCRStoreManager;
60 import org.mycore.datamodel.ifs2.MCRStoredMetadata;
61 import org.mycore.datamodel.ifs2.MCRVersionedMetadata;
62 import org.mycore.datamodel.ifs2.MCRVersioningMetadataStore;
63 import org.mycore.datamodel.metadata.MCRObject;
64 import org.mycore.datamodel.metadata.MCRObjectID;
65 import org.mycore.datamodel.metadata.history.MCRMetadataHistoryManager;
66 import org.xml.sax.SAXException;
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102 public class MCRDefaultXMLMetadataManager implements MCRXMLMetadataManagerAdapter {
103
104 private static final Logger LOGGER = LogManager.getLogger();
105
106 private static final String DEFAULT_SVN_DIRECTORY_NAME = "versions-metadata";
107
108
109 private static MCRDefaultXMLMetadataManager SINGLETON;
110
111 private HashSet<String> createdStores;
112
113
114
115
116 @SuppressWarnings("rawtypes")
117 private Class defaultClass;
118
119
120
121
122
123
124 private String defaultLayout;
125
126
127
128
129 private Path basePath;
130
131
132
133
134 private Path svnPath;
135
136
137
138
139 private URI svnBase;
140
141 protected MCRDefaultXMLMetadataManager() {
142 this.createdStores = new HashSet<>();
143 reload();
144 }
145
146
147 public static synchronized MCRDefaultXMLMetadataManager instance() {
148 if (SINGLETON == null) {
149 SINGLETON = new MCRDefaultXMLMetadataManager();
150 }
151 return SINGLETON;
152 }
153
154 public synchronized void reload() {
155 String pattern = MCRConfiguration2.getString("MCR.Metadata.ObjectID.NumberPattern").orElse("0000000000");
156 defaultLayout = pattern.length() - 4 + "-2-2";
157
158 String base = MCRConfiguration2.getStringOrThrow("MCR.Metadata.Store.BaseDir");
159 basePath = Paths.get(base);
160 checkPath(basePath, "base");
161
162 defaultClass = MCRConfiguration2.<MCRVersioningMetadataStore>getClass("MCR.Metadata.Store.DefaultClass")
163 .orElse(MCRVersioningMetadataStore.class);
164 if (MCRVersioningMetadataStore.class.isAssignableFrom(defaultClass)) {
165 Optional<String> svnBaseOpt = MCRConfiguration2.getString("MCR.Metadata.Store.SVNBase");
166 if (svnBaseOpt.isEmpty()) {
167 svnPath = Paths.get(MCRConfiguration2.getStringOrThrow("MCR.datadir"))
168 .resolve(DEFAULT_SVN_DIRECTORY_NAME);
169 checkPath(svnPath, "svn");
170 svnBase = svnPath.toUri();
171 } else {
172 try {
173 String svnBaseValue = svnBaseOpt.get();
174 if (!svnBaseValue.endsWith("/")) {
175 svnBaseValue += '/';
176 }
177 svnBase = new URI(svnBaseValue);
178 LOGGER.info("SVN Base: {}", svnBase);
179 if (svnBase.getScheme() == null) {
180 String workingDirectory = (new File(".")).getAbsolutePath();
181 URI root = new File(MCRConfiguration2.getString("MCR.datadir").orElse(workingDirectory))
182 .toURI();
183 URI resolved = root.resolve(svnBase);
184 LOGGER.warn("Resolved {} to {}", svnBase, resolved);
185 svnBase = resolved;
186 }
187 } catch (URISyntaxException ex) {
188 String msg = "Syntax error in MCR.Metadata.Store.SVNBase property: " + svnBase;
189 throw new MCRConfigurationException(msg, ex);
190 }
191 if (svnBase.getScheme().equals("file")) {
192 svnPath = Paths.get(svnBase);
193 checkPath(svnPath, "svn");
194 }
195 }
196 }
197 closeCreatedStores();
198 }
199
200 private synchronized void closeCreatedStores() {
201 for (String storeId : createdStores) {
202 MCRStoreCenter.instance().removeStore(storeId);
203 }
204 createdStores.clear();
205 }
206
207
208
209
210
211
212
213
214 private void checkPath(Path path, String type) {
215 if (!Files.exists(path)) {
216 try {
217 if (!Files.exists(Files.createDirectories(path))) {
218 throw new MCRConfigurationException(
219 "The metadata store " + type + " directory " + path.toAbsolutePath() + " does not exist.");
220 }
221 } catch (Exception ex) {
222 String msg = "Exception while creating metadata store " + type + " directory " + path.toAbsolutePath();
223 throw new MCRConfigurationException(msg, ex);
224 }
225 } else {
226 if (!Files.isDirectory(path)) {
227 throw new MCRConfigurationException(
228 "Metadata store " + type + " " + path.toAbsolutePath() + " is a file, not a directory");
229 }
230 if (!Files.isReadable(path)) {
231 throw new MCRConfigurationException(
232 "Metadata store " + type + " directory " + path.toAbsolutePath() + " is not readable");
233 }
234 if (!Files.isWritable(path)) {
235 throw new MCRConfigurationException(
236 "Metadata store " + type + " directory " + path.toAbsolutePath() + " is not writeable");
237 }
238 }
239 }
240
241
242
243
244
245
246 private MCRMetadataStore getStore(String base) {
247 String[] split = base.split("_");
248 return getStore(split[0], split[1], false);
249 }
250
251
252
253
254
255
256
257
258
259 private MCRMetadataStore getStore(String base, boolean readOnly) {
260 String[] split = base.split("_");
261 return getStore(split[0], split[1], readOnly);
262 }
263
264
265
266
267
268
269
270
271
272 private MCRMetadataStore getStore(MCRObjectID mcrid, boolean readOnly) {
273 return getStore(mcrid.getProjectId(), mcrid.getTypeId(), readOnly);
274 }
275
276
277
278
279
280
281
282
283
284 private MCRMetadataStore getStore(String project, String type, boolean readOnly) {
285 String projectType = getStoryKey(project, type);
286 String prefix = "MCR.IFS2.Store." + projectType + ".";
287 String forceXML = MCRConfiguration2.getString(prefix + "ForceXML").orElse(null);
288 if (forceXML == null) {
289 synchronized (this) {
290 forceXML = MCRConfiguration2.getString(prefix + "ForceXML").orElse(null);
291 if (forceXML == null) {
292 try {
293 setupStore(project, type, prefix, readOnly);
294 } catch (ReflectiveOperationException e) {
295 throw new MCRPersistenceException(
296 new MessageFormat("Could not instantiate store for project {0} and object type {1}.",
297 Locale.ROOT).format(new Object[] { project, type }),
298 e);
299 }
300 }
301 }
302 }
303 MCRMetadataStore store = MCRStoreManager.getStore(projectType);
304 if (store == null) {
305 throw new MCRPersistenceException(
306 new MessageFormat("Metadata store for project {0} and object type {1} is unconfigured.", Locale.ROOT)
307 .format(new Object[] { project, type }));
308 }
309 return store;
310 }
311
312 public void verifyStore(String base) {
313 MCRMetadataStore store = getStore(base);
314 if (store instanceof MCRVersioningMetadataStore) {
315 LOGGER.info("Verifying SVN history of {}.", base);
316 ((MCRVersioningMetadataStore) (getStore(base))).verify();
317 } else {
318 LOGGER.warn("Cannot verify unversioned store {}!", base);
319 }
320 }
321
322 @SuppressWarnings("unchecked")
323 private void setupStore(String project, String objectType, String configPrefix, boolean readOnly)
324 throws ReflectiveOperationException {
325 String baseID = getStoryKey(project, objectType);
326 Class<? extends MCRStore> clazz = MCRConfiguration2.<MCRStore>getClass(configPrefix + "Class")
327 .orElseGet(() -> {
328 MCRConfiguration2.set(configPrefix + "Class", defaultClass.getName());
329 return defaultClass;
330 });
331 if (MCRVersioningMetadataStore.class.isAssignableFrom(clazz)) {
332 String property = configPrefix + "SVNRepositoryURL";
333 String svnURL = MCRConfiguration2.getString(property).orElse(null);
334 if (svnURL == null) {
335 String relativeURI = new MessageFormat("{0}/{1}/", Locale.ROOT)
336 .format(new Object[] { project, objectType });
337 URI repURI = svnBase.resolve(relativeURI);
338 LOGGER.info("Resolved {} to {} for {}", relativeURI, repURI.toASCIIString(), property);
339 MCRConfiguration2.set(property, repURI.toASCIIString());
340 checkAndCreateDirectory(svnPath.resolve(project), project, objectType, configPrefix, readOnly);
341 }
342 }
343
344 Path typePath = basePath.resolve(project).resolve(objectType);
345 checkAndCreateDirectory(typePath, project, objectType, configPrefix, readOnly);
346
347 String slotLayout = MCRConfiguration2.getString(configPrefix + "SlotLayout").orElse(null);
348 if (slotLayout == null) {
349 MCRConfiguration2.set(configPrefix + "SlotLayout", defaultLayout);
350 }
351 MCRConfiguration2.set(configPrefix + "BaseDir", typePath.toAbsolutePath().toString());
352 MCRConfiguration2.set(configPrefix + "ForceXML", String.valueOf(true));
353 String value = "derivate".equals(objectType) ? "mycorederivate" : "mycoreobject";
354 MCRConfiguration2.set(configPrefix + "ForceDocType", value);
355 createdStores.add(baseID);
356 MCRStoreManager.createStore(baseID, clazz);
357 }
358
359 private void checkAndCreateDirectory(Path path, String project, String objectType, String configPrefix,
360 boolean readOnly) {
361 if (Files.exists(path)) {
362 return;
363 }
364 if (readOnly) {
365 throw new MCRPersistenceException(String.format(Locale.ENGLISH,
366 "Path does not exists ''%s'' to set up store for project ''%s'' and objectType ''%s'' "
367 + "and config prefix ''%s''. We are not willing to create it for an read only operation.",
368 path.toAbsolutePath(), project, objectType, configPrefix));
369 }
370 try {
371 if (!Files.exists(Files.createDirectories(path))) {
372 throw new FileNotFoundException(path.toAbsolutePath() + " does not exists.");
373 }
374 } catch (Exception e) {
375 throw new MCRPersistenceException(String.format(Locale.ENGLISH,
376 "Couldn'e create directory ''%s'' to set up store for project ''%s'' and objectType ''%s'' "
377 + "and config prefix ''%s''",
378 path.toAbsolutePath(), project, objectType, configPrefix));
379 }
380 }
381
382 private String getStoryKey(String project, String objectType) {
383 return project + "_" + objectType;
384 }
385
386 public void create(MCRObjectID mcrid, MCRContent xml, Date lastModified)
387 throws MCRPersistenceException {
388 try {
389 MCRStoredMetadata sm = getStore(mcrid, false).create(xml, mcrid.getNumberAsInteger());
390 sm.setLastModified(lastModified);
391 MCRConfigurationBase.systemModified();
392 } catch (Exception exc) {
393 throw new MCRPersistenceException("Error while storing object: " + mcrid, exc);
394 }
395 }
396
397 public void delete(MCRObjectID mcrid) throws MCRPersistenceException {
398 try {
399 getStore(mcrid, true).delete(mcrid.getNumberAsInteger());
400 MCRConfigurationBase.systemModified();
401 } catch (Exception exc) {
402 throw new MCRPersistenceException("Error while deleting object: " + mcrid, exc);
403 }
404 }
405
406 public void update(MCRObjectID mcrid, MCRContent xml, Date lastModified)
407 throws MCRPersistenceException {
408 if (!exists(mcrid)) {
409 throw new MCRPersistenceException("Object to update does not exist: " + mcrid);
410 }
411 try {
412 MCRStoredMetadata sm = getStore(mcrid, false).retrieve(mcrid.getNumberAsInteger());
413 sm.update(xml);
414 sm.setLastModified(lastModified);
415 MCRConfigurationBase.systemModified();
416 } catch (Exception exc) {
417 throw new MCRPersistenceException("Unable to update object " + mcrid, exc);
418 }
419 }
420
421 public MCRContent retrieveContent(MCRObjectID mcrid) throws IOException {
422 MCRContent metadata;
423 MCRStoredMetadata storedMetadata = retrieveStoredMetadata(mcrid);
424 if (storedMetadata == null || storedMetadata.isDeleted()) {
425 return null;
426 }
427 metadata = storedMetadata.getMetadata();
428 return metadata;
429 }
430
431 public MCRContent retrieveContent(MCRObjectID mcrid, String revision) throws IOException {
432 LOGGER.info("Getting object {} in revision {}", mcrid, revision);
433 MCRMetadataVersion version = getMetadataVersion(mcrid, Long.parseLong(revision));
434 if (version != null) {
435 MCRContent content = version.retrieve();
436 try {
437 Document doc = content.asXML();
438 doc.getRootElement().setAttribute("rev", version.getRevision());
439 return new MCRJDOMContent(doc);
440 } catch (JDOMException | SAXException e) {
441 throw new MCRPersistenceException("Could not parse XML from default store", e);
442 }
443 }
444 return null;
445 }
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460 private MCRMetadataVersion getMetadataVersion(MCRObjectID mcrId, long rev) throws IOException {
461 MCRVersionedMetadata versionedMetaData = getVersionedMetaData(mcrId);
462 if (versionedMetaData == null) {
463 return null;
464 }
465 return versionedMetaData.getRevision(rev);
466 }
467
468 public List<MCRMetadataVersion> listRevisions(MCRObjectID id) throws IOException {
469 MCRVersionedMetadata vm = getVersionedMetaData(id);
470 if (vm == null) {
471 return null;
472 }
473 return vm.listVersions();
474 }
475
476 private MCRVersionedMetadata getVersionedMetaData(MCRObjectID id) throws IOException {
477 if (id == null) {
478 return null;
479 }
480 MCRMetadataStore metadataStore = getStore(id, true);
481 if (!(metadataStore instanceof MCRVersioningMetadataStore)) {
482 return null;
483 }
484 MCRVersioningMetadataStore verStore = (MCRVersioningMetadataStore) metadataStore;
485 return verStore.retrieve(id.getNumberAsInteger());
486 }
487
488
489
490
491
492
493 private MCRStoredMetadata retrieveStoredMetadata(MCRObjectID mcrid) throws IOException {
494 return getStore(mcrid, true).retrieve(mcrid.getNumberAsInteger());
495 }
496
497 public int getHighestStoredID(String project, String type) {
498 MCRMetadataStore store;
499 try {
500 store = getStore(project, type, true);
501 } catch (MCRPersistenceException persistenceException) {
502
503 return 0;
504 }
505 int highestStoredID = store.getHighestStoredID();
506
507 return Math.max(highestStoredID, MCRMetadataHistoryManager.getHighestStoredID(project, type)
508 .map(MCRObjectID::getNumberAsInteger)
509 .orElse(0));
510 }
511
512 public boolean exists(MCRObjectID mcrid) throws MCRPersistenceException {
513 try {
514 if (mcrid == null) {
515 return false;
516 }
517 MCRMetadataStore store;
518 try {
519 store = getStore(mcrid, true);
520 } catch (MCRPersistenceException persistenceException) {
521
522 return false;
523 }
524 return store.exists(mcrid.getNumberAsInteger());
525 } catch (Exception exc) {
526 throw new MCRPersistenceException("Unable to check if object exists " + mcrid, exc);
527 }
528 }
529
530 public List<String> listIDsForBase(String base) {
531 MCRMetadataStore store;
532 try {
533 store = getStore(base, true);
534 } catch (MCRPersistenceException e) {
535 LOGGER.warn("Store for '{}' does not exist.", base);
536 return Collections.emptyList();
537 }
538
539 List<String> list = new ArrayList<>();
540 Iterator<Integer> it = store.listIDs(MCRStore.ASCENDING);
541 String[] idParts = MCRObjectID.getIDParts(base);
542 while (it.hasNext()) {
543 list.add(MCRObjectID.formatID(idParts[0], idParts[1], it.next()));
544 }
545 return list;
546 }
547
548 public List<String> listIDsOfType(String type) {
549 try (Stream<Path> streamBasePath = list(basePath)) {
550 return streamBasePath.flatMap(projectPath -> {
551 final String project = projectPath.getFileName().toString();
552 return list(projectPath).flatMap(typePath -> {
553 if (type.equals(typePath.getFileName().toString())) {
554 final String base = getStoryKey(project, type);
555 return listIDsForBase(base).stream();
556 }
557 return Stream.empty();
558 });
559 }).collect(Collectors.toList());
560 }
561 }
562
563 public List<String> listIDs() {
564 try (Stream<Path> streamBasePath = list(basePath)) {
565 return streamBasePath.flatMap(projectPath -> {
566 final String project = projectPath.getFileName().toString();
567 return list(projectPath).flatMap(typePath -> {
568 final String type = typePath.getFileName().toString();
569 final String base = getStoryKey(project, type);
570 return listIDsForBase(base).stream();
571 });
572 }).collect(Collectors.toList());
573 }
574 }
575
576 public Collection<String> getObjectTypes() {
577 try (Stream<Path> streamBasePath = list(basePath)) {
578 return streamBasePath.flatMap(this::list)
579 .map(Path::getFileName)
580 .map(Path::toString)
581 .filter(MCRObjectID::isValidType)
582 .distinct()
583 .collect(Collectors.toSet());
584 }
585 }
586
587 public Collection<String> getObjectBaseIds() {
588 try (Stream<Path> streamBasePath = list(basePath)) {
589 return streamBasePath.flatMap(this::list)
590 .filter(p -> MCRObjectID.isValidType(p.getFileName().toString()))
591 .map(p -> p.getParent().getFileName() + "_" + p.getFileName())
592 .collect(Collectors.toSet());
593 }
594 }
595
596
597
598
599
600
601 private Stream<Path> list(Path path) {
602 try {
603 return Files.list(path);
604 } catch (IOException ioException) {
605 throw new MCRPersistenceException(
606 "unable to list files of IFS2 metadata directory " + path.toAbsolutePath(), ioException);
607 }
608 }
609
610 public List<MCRObjectIDDate> retrieveObjectDates(List<String> ids) throws IOException {
611 List<MCRObjectIDDate> objidlist = new ArrayList<>(ids.size());
612 for (String id : ids) {
613 MCRStoredMetadata sm = this.retrieveStoredMetadata(MCRObjectID.getInstance(id));
614 objidlist.add(new MCRObjectIDFileSystemDate(sm, id));
615 }
616 return objidlist;
617 }
618
619 public long getLastModified(MCRObjectID id) throws IOException {
620 MCRMetadataStore store = getStore(id, true);
621 MCRStoredMetadata metadata = store.retrieve(id.getNumberAsInteger());
622 if (metadata != null) {
623 return metadata.getLastModified().getTime();
624 }
625 return -1;
626 }
627
628 public MCRCache.ModifiedHandle getLastModifiedHandle(final MCRObjectID id, final long expire, TimeUnit unit) {
629 return new StoreModifiedHandle(this, id, expire, unit);
630 }
631
632 private static final class StoreModifiedHandle implements MCRCache.ModifiedHandle {
633 private final MCRDefaultXMLMetadataManager mm;
634
635 private final long expire;
636
637 private final MCRObjectID id;
638
639 private StoreModifiedHandle(MCRDefaultXMLMetadataManager mm, MCRObjectID id, long time, TimeUnit unit) {
640 this.mm = mm;
641 this.expire = unit.toMillis(time);
642 this.id = id;
643 }
644
645 @Override
646 public long getCheckPeriod() {
647 return expire;
648 }
649
650 @Override
651 public long getLastModified() throws IOException {
652 return mm.getLastModified(id);
653 }
654 }
655 }