1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.datamodel.ifs2;
20
21 import java.io.IOException;
22 import java.net.URI;
23 import java.net.URISyntaxException;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.Paths;
27 import java.nio.file.attribute.BasicFileAttributes;
28 import java.text.NumberFormat;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.NoSuchElementException;
35 import java.util.Spliterator;
36 import java.util.Spliterators;
37 import java.util.StringTokenizer;
38 import java.util.function.Function;
39 import java.util.stream.IntStream;
40 import java.util.stream.Stream;
41 import java.util.stream.StreamSupport;
42
43 import org.apache.logging.log4j.LogManager;
44 import org.apache.logging.log4j.Logger;
45 import org.mycore.common.MCRException;
46 import org.mycore.common.config.MCRConfigurationException;
47 import org.mycore.datamodel.niofs.utils.MCRRecursiveDeleter;
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 public abstract class MCRStore {
82
83
84
85
86 public static final boolean ASCENDING = true;
87
88
89
90
91 public static final boolean DESCENDING = false;
92
93
94 protected String id;
95
96
97 protected Path baseDirectory;
98
99
100 protected int idLength;
101
102
103
104
105
106 protected int[] slotLength;
107
108
109 protected String prefix = "";
110
111
112 protected String suffix = "";
113
114 private MCRStoreConfig storeConfig;
115
116 private Function<String, String> toNativePath;
117
118
119
120
121
122
123
124
125
126
127
128
129 protected int offset = 11;
130
131
132
133
134 protected int lastID = 0;
135
136 public static final Logger LOGGER = LogManager.getLogger();
137
138
139
140
141
142
143
144 public void delete(final int id) throws IOException {
145 delete(getSlot(id));
146 }
147
148
149
150
151
152
153
154
155 public boolean exists(final int id) throws IOException {
156 return Files.exists(getSlot(id));
157 }
158
159 public synchronized int getHighestStoredID() {
160 try {
161 String max = findMaxID(baseDirectory, 0);
162 if (max != null) {
163 return slot2id(max);
164 }
165 } catch (final IOException e) {
166 LOGGER.error("Error while getting highest stored ID in " + baseDirectory, e);
167 }
168 return 0;
169 }
170
171
172
173
174 public String getID() {
175 return getStoreConfig().getID();
176 }
177
178
179
180
181
182
183
184
185 public synchronized int getNextFreeID() {
186 lastID = Math.max(getHighestStoredID(), lastID);
187 lastID += lastID > 0 ? offset : 1;
188 offset = 1;
189 return lastID;
190 }
191
192 public boolean isEmpty() {
193 try (Stream<Path> streamBaseDirectory = Files.list(baseDirectory)) {
194 return streamBaseDirectory.findAny().isEmpty();
195 } catch (final IOException e) {
196 LOGGER.error("Error while checking if base directory is empty: " + baseDirectory, e);
197 return false;
198 }
199 }
200
201
202
203
204 public IntStream getStoredIDs() {
205 int characteristics = Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.SORTED;
206 return StreamSupport
207 .stream(() -> Spliterators
208 .spliteratorUnknownSize(listIDs(ASCENDING), characteristics),
209 characteristics,
210 false)
211 .mapToInt(Integer::intValue);
212 }
213
214
215
216
217
218
219
220
221
222
223
224
225 public Iterator<Integer> listIDs(final boolean order) {
226 return new Iterator<Integer>() {
227
228
229
230 List<Path> files = new ArrayList<>();
231
232
233
234
235 int nextID;
236
237
238
239
240 int lastID;
241
242
243
244
245
246 boolean order;
247
248 @Override
249 public boolean hasNext() {
250 return nextID > 0;
251 }
252
253 @Override
254 public Integer next() {
255 if (nextID < 1) {
256 throw new NoSuchElementException();
257 }
258
259 lastID = nextID;
260 nextID = findNextID();
261 return lastID;
262 }
263
264 @Override
265 public void remove() {
266 if (lastID == 0) {
267 throw new IllegalStateException();
268 }
269 try {
270 MCRStore.this.delete(lastID);
271 } catch (final Exception ex) {
272 throw new MCRException("Could not delete " + MCRStore.this.getID() + " " + lastID, ex);
273 }
274 lastID = 0;
275 }
276
277
278
279
280
281
282
283
284 Iterator<Integer> init(final boolean order) {
285 this.order = order;
286 try {
287 addChildren(baseDirectory);
288 } catch (final IOException e) {
289 LOGGER.error("Error while iterating over children of " + baseDirectory, e);
290 }
291 nextID = findNextID();
292 return this;
293 }
294
295
296
297
298
299
300
301
302
303
304 private void addChildren(final Path dir) throws IOException {
305 if (Files.isDirectory(dir)) {
306 try (Stream<Path> steamDir = Files.list(dir)) {
307 final Path[] children = steamDir.toArray(Path[]::new);
308 Arrays.sort(children, new MCRPathComparator());
309
310 for (int i = 0; i < children.length; i++) {
311 files.add(order ? i : 0, children[i]);
312 }
313 }
314 }
315 }
316
317
318
319
320
321
322 private int findNextID() {
323 if (files.isEmpty()) {
324 return 0;
325 }
326
327 final Path first = files.remove(0);
328
329
330 String fileName = first.getFileName().toString();
331 if (fileName.length() == idLength + prefix.length() + suffix.length()) {
332 return MCRStore.this.slot2id(fileName);
333 }
334
335 try {
336 addChildren(first);
337 } catch (final IOException e) {
338 LOGGER.error("Error while finding next id.", e);
339 }
340 return findNextID();
341 }
342 }.init(order);
343 }
344
345
346
347
348
349
350
351
352 void delete(final Path path) throws IOException {
353 if (!path.startsWith(baseDirectory)) {
354 throw new IllegalArgumentException(path + " is not in the base directory " + baseDirectory);
355 }
356 Path current = path;
357 Path parent = path.getParent();
358 Files.walkFileTree(path, MCRRecursiveDeleter.instance());
359
360 while (!Files.isSameFile(baseDirectory, parent)) {
361
362
363 try (Stream<Path> streamParent = Files.list(parent)) {
364 if (streamParent.findAny().isPresent()) {
365 break;
366 }
367 current = parent;
368 parent = current.getParent();
369 Files.delete(current);
370 }
371 }
372 }
373
374
375
376
377 public Path getBaseDirectory() {
378 return baseDirectory.toAbsolutePath();
379 }
380
381
382
383
384
385
386 String getBaseDirURI() {
387 return baseDirectory.toAbsolutePath().toUri().toString();
388 }
389
390
391 int getIDLength() {
392 return idLength;
393 }
394
395
396
397
398
399
400
401
402
403 String getSlotPath(final int id) {
404 final String[] paths = getSlotPaths(id);
405 return paths[paths.length - 1];
406 }
407
408
409
410
411
412
413
414
415
416
417 String[] getSlotPaths(final int id) {
418 final String paddedId = createIDWithLeadingZeros(id);
419
420 final String[] paths = new String[slotLength.length + 1];
421 final StringBuilder path = new StringBuilder();
422 int offset = 0;
423 for (int i = 0; i < paths.length - 1; i++) {
424 path.append(paddedId, offset, offset + slotLength[i]);
425 paths[i] = path.toString();
426 path.append("/");
427 offset += slotLength[i];
428 }
429 path.append(prefix).append(paddedId).append(suffix);
430 paths[paths.length - 1] = path.toString();
431 return paths;
432 }
433
434
435
436
437
438
439
440
441 int slot2id(String slot) {
442 slot = slot.substring(prefix.length());
443 slot = slot.substring(0, idLength);
444 return Integer.parseInt(slot);
445 }
446
447
448
449
450
451
452
453
454
455
456 protected Path getSlot(final int id) throws IOException {
457 String slotPath = getSlotPath(id);
458 return baseDirectory.resolve(toNativePath.apply(slotPath));
459 }
460
461 protected MCRStoreConfig getStoreConfig() {
462 return storeConfig;
463 }
464
465 protected void init(final MCRStoreConfig config) {
466 setStoreConfig(config);
467
468 idLength = 0;
469
470 final StringTokenizer st = new StringTokenizer(getStoreConfig().getSlotLayout(), "-");
471 slotLength = new int[st.countTokens() - 1];
472
473 int i = 0;
474 while (st.countTokens() > 1) {
475 slotLength[i] = Integer.parseInt(st.nextToken());
476 idLength += slotLength[i++];
477 }
478 idLength += Integer.parseInt(st.nextToken());
479 prefix = config.getPrefix();
480
481 try {
482 try {
483 URI uri = new URI(getStoreConfig().getBaseDir());
484 if (uri.getScheme() != null) {
485 baseDirectory = Paths.get(uri);
486 }
487 } catch (URISyntaxException e) {
488
489 }
490 if (baseDirectory == null) {
491 baseDirectory = Paths.get(getStoreConfig().getBaseDir());
492 }
493
494 String separator = baseDirectory.getFileSystem().getSeparator();
495 if (separator.equals("/")) {
496 toNativePath = s -> s;
497 } else {
498 toNativePath = s -> {
499 if (s.contains("/")) {
500 if (s.contains(separator)) {
501 throw new IllegalArgumentException(
502 s + " may not contain both '/' and '" + separator + "'.");
503 }
504 return s.replace("/", separator);
505 }
506 return s;
507 };
508 }
509
510 try {
511 BasicFileAttributes attrs = Files.readAttributes(baseDirectory, BasicFileAttributes.class);
512 if (!attrs.isDirectory()) {
513 final String msg = "Store " + getStoreConfig().getBaseDir() + " is not a directory";
514 throw new MCRConfigurationException(msg);
515 }
516
517 if (!Files.isReadable(baseDirectory)) {
518 final String msg = "Store directory " + getStoreConfig().getBaseDir() + " is not readable";
519 throw new MCRConfigurationException(msg);
520 }
521 } catch (IOException e) {
522
523 Files.createDirectories(baseDirectory);
524 }
525 } catch (final IOException e) {
526 LOGGER.error("Could not initialize store " + config.getID() + " correctly.", e);
527 }
528 }
529
530
531
532
533 protected void init(final String id) {
534 init(new MCRStoreDefaultConfig(id));
535 }
536
537 protected void setStoreConfig(final MCRStoreConfig storeConfig) {
538 this.storeConfig = storeConfig;
539 }
540
541 private String createIDWithLeadingZeros(final int id) {
542 final NumberFormat numWithLeadingZerosFormat = NumberFormat.getIntegerInstance(Locale.ROOT);
543 numWithLeadingZerosFormat.setMinimumIntegerDigits(idLength);
544 numWithLeadingZerosFormat.setGroupingUsed(false);
545 return numWithLeadingZerosFormat.format(id);
546 }
547
548
549
550
551
552
553
554
555
556
557
558 private String findMaxID(final Path dir, final int depth) throws IOException {
559
560 final Path[] children;
561
562 try (Stream<Path> streamDirectory = Files.list(dir)) {
563 children = streamDirectory.toArray(Path[]::new);
564 }
565
566 if (children.length == 0) {
567 return null;
568 }
569
570 Arrays.sort(children, new MCRPathComparator());
571
572 if (depth == slotLength.length) {
573 return children[children.length - 1].getFileName().toString();
574 }
575
576 for (int i = children.length - 1; i >= 0; i--) {
577 final Path child = children[i];
578
579 try (Stream<Path> streamChild = Files.list(child)) {
580 if (!Files.isDirectory(child) || streamChild.findAny().isEmpty()) {
581 continue;
582 }
583 }
584
585 final String found = findMaxID(child, depth + 1);
586 if (found != null) {
587 return found;
588 }
589 }
590 return null;
591 }
592
593 public interface MCRStoreConfig {
594 String getBaseDir();
595
596 String getID();
597
598 String getPrefix();
599
600 String getSlotLayout();
601 }
602 }