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.common;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.PrintWriter;
25  import java.io.Reader;
26  import java.io.StringWriter;
27  import java.lang.reflect.Constructor;
28  import java.lang.reflect.InvocationTargetException;
29  import java.nio.charset.Charset;
30  import java.nio.charset.StandardCharsets;
31  import java.nio.file.FileSystem;
32  import java.nio.file.FileVisitResult;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.nio.file.Paths;
36  import java.nio.file.SimpleFileVisitor;
37  import java.nio.file.StandardCopyOption;
38  import java.nio.file.StandardOpenOption;
39  import java.nio.file.attribute.FileTime;
40  import java.security.MessageDigest;
41  import java.security.NoSuchAlgorithmException;
42  import java.text.Normalizer;
43  import java.text.Normalizer.Form;
44  import java.time.Duration;
45  import java.util.ArrayList;
46  import java.util.Collections;
47  import java.util.HashMap;
48  import java.util.Locale;
49  import java.util.Optional;
50  import java.util.Objects;
51  import java.util.Properties;
52  import java.util.concurrent.TimeUnit;
53  import java.util.function.Consumer;
54  import java.util.function.Function;
55  import java.util.stream.Stream;
56  
57  import javax.xml.parsers.SAXParser;
58  import javax.xml.parsers.SAXParserFactory;
59  
60  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
61  import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
62  import org.apache.commons.io.IOUtils;
63  import org.apache.logging.log4j.LogManager;
64  import org.apache.logging.log4j.Logger;
65  import org.mycore.common.config.MCRConfigurationException;
66  import org.mycore.common.content.streams.MCRDevNull;
67  import org.mycore.common.content.streams.MCRMD5InputStream;
68  import org.mycore.common.function.MCRThrowableTask;
69  import org.mycore.datamodel.niofs.MCRPathUtils;
70  import org.xml.sax.Attributes;
71  import org.xml.sax.InputSource;
72  import org.xml.sax.helpers.DefaultHandler;
73  
74  import jakarta.xml.bind.DatatypeConverter;
75  
76  /**
77   * This class represent a general set of external methods to support the programming API.
78   *
79   * @author Jens Kupferschmidt
80   * @author Frank Lützenkirchen
81   * @author Thomas Scheffler (yagee)
82   * @version $Revision$ $Date$
83   */
84  public class MCRUtils {
85      // public constant data
86      private static final Logger LOGGER = LogManager.getLogger();
87  
88      // The file slash
89      private static String SLASH = System.getProperty("file.separator");
90  
91      /**
92       * Reads exactly <code>len</code> bytes from the input stream into the byte array. This method reads repeatedly from
93       * the underlying stream until all the bytes are read. InputStream.read is often documented to block like this, but
94       * in actuality it does not always do so, and returns early with just a few bytes. readBlockiyng blocks until all
95       * the bytes are read, the end of the stream is detected, or an exception is thrown. You will always get as many
96       * bytes as you asked for unless you get an eof or other exception. Unlike readFully, you find out how many bytes
97       * you did get.
98       *
99       * @param b
100      *            the buffer into which the data is read.
101      * @param off
102      *            the start offset of the data.
103      * @param len
104      *            the number of bytes to read.
105      * @return number of bytes actually read.
106      * @exception IOException
107      *                if an I/O error occurs.
108      */
109     public static int readBlocking(InputStream in, byte[] b, int off, int len) throws IOException {
110         int totalBytesRead = 0;
111 
112         while (totalBytesRead < len) {
113             int bytesRead = in.read(b, off + totalBytesRead, len - totalBytesRead);
114 
115             if (bytesRead < 0) {
116                 break;
117             }
118 
119             totalBytesRead += bytesRead;
120         }
121 
122         return totalBytesRead;
123     } // end readBlocking
124 
125     /**
126      * Reads exactly <code>len</code> bytes from the input stream into the byte array. This method reads repeatedly from
127      * the underlying stream until all the bytes are read. Reader.read is often documented to block like this, but in
128      * actuality it does not always do so, and returns early with just a few bytes. readBlockiyng blocks until all the
129      * bytes are read, the end of the stream is detected, or an exception is thrown. You will always get as many bytes
130      * as you asked for unless you get an eof or other exception. Unlike readFully, you find out how many bytes you did
131      * get.
132      *
133      * @param c
134      *            the buffer into which the data is read.
135      * @param off
136      *            the start offset of the data.
137      * @param len
138      *            the number of bytes to read.
139      * @return number of bytes actually read.
140      * @exception IOException
141      *                if an I/O error occurs.
142      */
143     public static int readBlocking(Reader in, char[] c, int off, int len) throws IOException {
144         int totalCharsRead = 0;
145 
146         while (totalCharsRead < len) {
147             int charsRead = in.read(c, off + totalCharsRead, len - totalCharsRead);
148 
149             if (charsRead < 0) {
150                 break;
151             }
152 
153             totalCharsRead += charsRead;
154         }
155 
156         return totalCharsRead;
157     } // end readBlocking
158 
159     /**
160      * Writes plain text to a file.
161      *
162      * @param textToWrite
163      *            the text to write into the file
164      * @param fileName
165      *            the name of the file to write to, given as absolute path
166      * @return a handle to the written file
167      */
168     public static Path writeTextToFile(String textToWrite, String fileName, Charset cs) throws IOException {
169         Path file = Paths.get(fileName);
170         Files.write(file, Collections.singletonList(textToWrite), cs, StandardOpenOption.CREATE);
171         return file;
172     }
173 
174     /**
175      * The method return a list of all file names under the given directory and subdirectories of itself.
176      *
177      * @param basedir
178      *            the File instance of the basic directory
179      * @return an ArrayList with file names as pathes
180      */
181     public static ArrayList<String> getAllFileNames(File basedir) {
182         ArrayList<String> out = new ArrayList<>();
183         File[] stage = basedir.listFiles();
184 
185         for (File element : stage) {
186             if (element.isFile()) {
187                 out.add(element.getName());
188             }
189 
190             if (element.isDirectory()) {
191                 out.addAll(getAllFileNames(element, element.getName() + SLASH));
192             }
193         }
194 
195         return out;
196     }
197 
198     /**
199      * The method return a list of all file names under the given directory and subdirectories of itself.
200      *
201      * @param basedir
202      *            the File instance of the basic directory
203      * @param path
204      *            the part of directory path
205      * @return an ArrayList with file names as pathes
206      */
207     public static ArrayList<String> getAllFileNames(File basedir, String path) {
208         ArrayList<String> out = new ArrayList<>();
209         File[] stage = basedir.listFiles();
210 
211         for (File element : stage) {
212             if (element.isFile()) {
213                 out.add(path + element.getName());
214             }
215 
216             if (element.isDirectory()) {
217                 out.addAll(getAllFileNames(element, path + element.getName() + SLASH));
218             }
219         }
220 
221         return out;
222     }
223 
224     /**
225      * The method return a list of all directory names under the given directory and subdirectories of itself.
226      *
227      * @param basedir
228      *            the File instance of the basic directory
229      * @return an ArrayList with directory names as pathes
230      */
231     public static ArrayList<String> getAllDirectoryNames(File basedir) {
232         ArrayList<String> out = new ArrayList<>();
233         File[] stage = basedir.listFiles();
234 
235         for (File element : stage) {
236             if (element.isDirectory()) {
237                 out.add(element.getName());
238                 out.addAll(getAllDirectoryNames(element, element.getName() + SLASH));
239             }
240         }
241 
242         return out;
243     }
244 
245     /**
246      * The method return a list of all directory names under the given directory and subdirectories of itself.
247      *
248      * @param basedir
249      *            the File instance of the basic directory
250      * @param path
251      *            the part of directory path
252      * @return an ArrayList with directory names as pathes
253      */
254     public static ArrayList<String> getAllDirectoryNames(File basedir, String path) {
255         ArrayList<String> out = new ArrayList<>();
256         File[] stage = basedir.listFiles();
257 
258         for (File element : stage) {
259             if (element.isDirectory()) {
260                 out.add(path + element.getName());
261                 out.addAll(getAllDirectoryNames(element, path + element.getName() + SLASH));
262             }
263         }
264 
265         return out;
266     }
267 
268     public static String parseDocumentType(InputStream in) {
269         SAXParser parser = null;
270 
271         try {
272             parser = SAXParserFactory.newInstance().newSAXParser();
273         } catch (Exception ex) {
274             String msg = "Could not build a SAX Parser for processing XML input";
275             throw new MCRConfigurationException(msg, ex);
276         }
277 
278         final Properties detected = new Properties();
279         final String forcedInterrupt = "mcr.forced.interrupt";
280 
281         DefaultHandler handler = new DefaultHandler() {
282             @Override
283             public void startElement(String uri, String localName, String qName, Attributes attributes) {
284                 LOGGER.debug("MCRLayoutService detected root element = {}", qName);
285                 detected.setProperty("docType", qName);
286                 throw new MCRException(forcedInterrupt);
287             }
288         };
289 
290         try {
291             parser.parse(new InputSource(in), handler);
292         } catch (Exception ex) {
293             if (!forcedInterrupt.equals(ex.getMessage())) {
294                 String msg = "Error while detecting XML document type from input source";
295                 throw new MCRException(msg, ex);
296             }
297         }
298 
299         String docType = detected.getProperty("docType");
300         int pos = docType.indexOf(':') + 1;
301         if (pos > 0) {
302             //filter namespace prefix
303             docType = docType.substring(pos);
304         }
305         return docType;
306 
307     }
308 
309     public static String asSHA1String(int iterations, byte[] salt, String text) throws NoSuchAlgorithmException {
310         return getHash(iterations, salt, text, "SHA-1");
311     }
312 
313     public static String asSHA256String(int iterations, byte[] salt, String text) throws NoSuchAlgorithmException {
314         return getHash(iterations, salt, text, "SHA-256");
315     }
316 
317     public static String asMD5String(int iterations, byte[] salt, String text) throws NoSuchAlgorithmException {
318         return getHash(iterations, salt, text, "MD5");
319     }
320 
321     public static String asCryptString(String salt, String text) {
322         return MCRCrypt.crypt(salt, text);
323     }
324 
325     private static String getHash(int iterations, byte[] salt, String text, String algorithm)
326         throws NoSuchAlgorithmException {
327         MessageDigest digest;
328         if (--iterations < 0) {
329             iterations = 0;
330         }
331         byte[] data;
332         digest = MessageDigest.getInstance(algorithm);
333         text = Normalizer.normalize(text, Form.NFC);
334         if (salt != null) {
335             digest.update(salt);
336         }
337         data = digest.digest(text.getBytes(StandardCharsets.UTF_8));
338         for (int i = 0; i < iterations; i++) {
339             data = digest.digest(data);
340         }
341         return toHexString(data);
342     }
343 
344     public static String toHexString(byte[] data) {
345         return DatatypeConverter.printHexBinary(data).toLowerCase(Locale.ROOT);
346     }
347 
348     /**
349      * Calculates md5 sum of InputStream. InputStream is consumed after calling this method and automatically closed.
350      */
351     public static String getMD5Sum(InputStream inputStream) throws IOException {
352         MCRMD5InputStream md5InputStream = null;
353         try {
354             md5InputStream = new MCRMD5InputStream(inputStream);
355             IOUtils.copy(md5InputStream, new MCRDevNull());
356             return md5InputStream.getMD5String();
357         } finally {
358             if (md5InputStream != null) {
359                 md5InputStream.close();
360             }
361         }
362     }
363 
364     /**
365      * Extracts files in a tar archive. Currently works only on uncompressed tar files.
366      *
367      * @param source
368      *            the uncompressed tar to extract
369      * @param expandToDirectory
370      *            the directory to extract the tar file to
371      * @throws IOException
372      *             if the source file does not exists
373      */
374     public static void untar(Path source, Path expandToDirectory) throws IOException {
375         try (TarArchiveInputStream tain = new TarArchiveInputStream(Files.newInputStream(source))) {
376             TarArchiveEntry tarEntry;
377             FileSystem targetFS = expandToDirectory.getFileSystem();
378             HashMap<Path, FileTime> directoryTimes = new HashMap<>();
379             while ((tarEntry = tain.getNextTarEntry()) != null) {
380                 Path target = MCRPathUtils.getPath(targetFS, tarEntry.getName());
381                 Path absoluteTarget = expandToDirectory.resolve(target).normalize().toAbsolutePath();
382                 if (tarEntry.isDirectory()) {
383                     Files.createDirectories(expandToDirectory.resolve(absoluteTarget));
384                     directoryTimes.put(absoluteTarget, FileTime.fromMillis(tarEntry.getLastModifiedDate().getTime()));
385                 } else {
386                     if (Files.notExists(absoluteTarget.getParent())) {
387                         Files.createDirectories(absoluteTarget.getParent());
388                     }
389                     Files.copy(tain, absoluteTarget, StandardCopyOption.REPLACE_EXISTING);
390                     Files.setLastModifiedTime(absoluteTarget,
391                         FileTime.fromMillis(tarEntry.getLastModifiedDate().getTime()));
392                 }
393             }
394             //restore directory dates
395             Files.walkFileTree(expandToDirectory, new SimpleFileVisitor<Path>() {
396                 @Override
397                 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
398                     Path absolutePath = dir.normalize().toAbsolutePath();
399                     FileTime lastModifiedTime = directoryTimes.get(absolutePath);
400                     if (lastModifiedTime != null) {
401                         Files.setLastModifiedTime(absolutePath, lastModifiedTime);
402                     } else {
403                         LOGGER.warn("Could not restore last modified time for {} from TAR file {}.", absolutePath,
404                             source);
405                     }
406                     return super.postVisitDirectory(dir, exc);
407                 }
408             });
409         }
410     }
411 
412     @SafeVarargs
413     public static Exception unwrapExCeption(Exception e, Class<? extends Exception>... classes) {
414         if (classes.length == 0) {
415             return e;
416         }
417         Class<? extends Exception> mainExceptionClass = classes[0];
418         Throwable check = e;
419         for (Class<? extends Exception> instChk : classes) {
420             if (instChk.isInstance(check)) {
421                 return (Exception) check;
422             }
423             check = check.getCause();
424             if (check == null) {
425                 break;
426             }
427         }
428         @SuppressWarnings("unchecked")
429         Constructor<? extends Exception>[] constructors = (Constructor<? extends Exception>[]) mainExceptionClass
430             .getConstructors();
431         for (Constructor<? extends Exception> c : constructors) {
432             Class<?>[] parameterTypes = c.getParameterTypes();
433             try {
434                 if (parameterTypes.length == 0) {
435                     Exception exception = c.newInstance((Object[]) null);
436                     exception.initCause(e);
437                     return exception;
438                 }
439                 if (parameterTypes.length == 1 && parameterTypes[0].isAssignableFrom(mainExceptionClass)) {
440                     return c.newInstance(e);
441                 }
442             } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
443                 | InvocationTargetException ex) {
444                 LOGGER.warn("Exception while initializing exception {}", mainExceptionClass.getCanonicalName(), ex);
445                 return e;
446             }
447         }
448         LOGGER.warn("Could not instanciate Exception {}", mainExceptionClass.getCanonicalName());
449         return e;
450     }
451 
452     /**
453      * Takes a file size in bytes and formats it as a string for output. For values &lt; 5 KB the output format is for
454      * example "320 Byte". For values &gt; 5 KB the output format is for example "6,8 KB". For values &gt; 1 MB the
455      * output format is for example "3,45 MB".
456      */
457     public static String getSizeFormatted(long bytes) {
458         String sizeUnit;
459         String sizeText;
460         double sizeValue;
461 
462         if (bytes >= 1024 * 1024) {
463             // >= 1 MB
464             sizeUnit = "MB";
465             sizeValue = (double) Math.round(bytes / 10485.76) / 100;
466         } else if (bytes >= 5 * 1024) {
467             // >= 5 KB
468             sizeUnit = "KB";
469             sizeValue = (double) Math.round(bytes / 102.4) / 10;
470         } else {
471             // < 5 KB
472             sizeUnit = "Byte";
473             sizeValue = bytes;
474         }
475 
476         sizeText = String.valueOf(sizeValue).replace('.', ',');
477 
478         if (sizeText.endsWith(",0")) {
479             sizeText = sizeText.substring(0, sizeText.length() - 2);
480         }
481 
482         return sizeText + " " + sizeUnit;
483     }
484 
485     /**
486      * Helps to implement {@link Comparable#compareTo(Object)}
487      *
488      * For every <code>part</code> a check is performed in the specified order.
489      * The first check that does not return <code>0</code> the result is returned
490      * by this method. So when this method returns <code>0</code> <code>first</code>
491      * and <code>other</code> should be the same.
492      *
493      * @param first first Object that should be compared
494      * @param other Object that first should be compared against, e.g. <code>first.compareTo(other)</code>
495      * @param part different <code>compareTo()</code> steps
496      * @param <T> object that wants to implement compareTo()
497      * @return a negative integer, zero, or a positive integer as this object
498      *          is less than, equal to, or greater than the specified object.
499      *
500      * @throws NullPointerException if either <code>first</code> or <code>other</code> is null
501      */
502     @SafeVarargs
503     @SuppressWarnings("unchecked")
504     public static <T> int compareParts(T first, T other, Function<T, Comparable>... part) {
505         return Stream.of(part)
506             .mapToInt(f -> f.apply(first).compareTo(f.apply(other)))
507             .filter(i -> i != 0)
508             .findFirst()
509             .orElse(0);
510     }
511 
512     /**
513      * @param t contains the printStackTrace
514      * @return the stacktrace as string
515      */
516     public static String getStackTraceAsString(Throwable t) {
517         StringWriter sw = new StringWriter();
518         t.printStackTrace(new PrintWriter(sw)); // closing string writer has no effect
519         return sw.toString();
520     }
521 
522     /**
523      * Checks is trimmed <code>value</code> is not empty.
524      * @param value String to test
525      * @return <em>empty</em> if value is <code>null</code> or empty after trimming.
526      */
527     public static Optional<String> filterTrimmedNotEmpty(String value) {
528         return Optional.ofNullable(value)
529             .map(String::trim)
530             .filter(s -> !s.isEmpty());
531     }
532 
533     /**
534      * Measures the time of a method call.
535      * timeHandler is guaranteed to be called even if exception is thrown.
536      * @param unit time unit for timeHandler
537      * @param timeHandler gets the duration in <code>unit</code>
538      * @param task method reference
539      * @throws T if task.run() throws Exception
540      */
541     public static <T extends Throwable> void measure(TimeUnit unit, Consumer<Long> timeHandler,
542         MCRThrowableTask<T> task) throws T {
543         long time = System.nanoTime();
544         try {
545             task.run();
546         } finally {
547             time = System.nanoTime() - time;
548             timeHandler.accept(unit.convert(time, TimeUnit.NANOSECONDS));
549         }
550     }
551 
552     /**
553      * Measures and logs the time of a method call
554      * @param task method reference
555      * @throws T if task.run() throws Exception
556      */
557     public static <T extends Throwable> Duration measure(MCRThrowableTask<T> task) throws T {
558         long time = System.nanoTime();
559         task.run();
560         time = System.nanoTime() - time;
561         return Duration.of(time, TimeUnit.NANOSECONDS.toChronoUnit());
562     }
563 
564     public static Path safeResolve(Path basePath, Path resolve) {
565         Path absoluteBasePath = Objects.requireNonNull(basePath)
566             .toAbsolutePath();
567         final Path resolved = absoluteBasePath
568             .resolve(Objects.requireNonNull(resolve))
569             .normalize();
570 
571         if (resolved.startsWith(absoluteBasePath)) {
572             return resolved;
573         }
574         throw new MCRException("Bad path: " + resolve);
575     }
576 
577     public static Path safeResolve(Path basePath, String... resolve) {
578         if (resolve.length == 0) {
579             return basePath;
580         }
581 
582         String[] more = Stream.of(resolve).skip(1).toArray(String[]::new);
583         final Path resolvePath = basePath.getFileSystem().getPath(resolve[0], more);
584         return safeResolve(basePath, resolvePath);
585     }
586 }