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.iview2.services;
20  
21  import java.awt.Graphics;
22  import java.awt.image.BufferedImage;
23  import java.io.IOException;
24  import java.io.UncheckedIOException;
25  import java.net.URI;
26  import java.nio.channels.SeekableByteChannel;
27  import java.nio.file.FileStore;
28  import java.nio.file.FileSystem;
29  import java.nio.file.FileSystemAlreadyExistsException;
30  import java.nio.file.FileSystemNotFoundException;
31  import java.nio.file.FileSystems;
32  import java.nio.file.Files;
33  import java.nio.file.NoSuchFileException;
34  import java.nio.file.Path;
35  import java.nio.file.Paths;
36  import java.text.MessageFormat;
37  import java.util.Collections;
38  import java.util.Locale;
39  import java.util.Optional;
40  import java.util.stream.Collectors;
41  
42  import javax.imageio.ImageIO;
43  import javax.imageio.ImageReader;
44  import javax.imageio.stream.ImageInputStream;
45  
46  import org.apache.logging.log4j.LogManager;
47  import org.apache.logging.log4j.Logger;
48  import org.jdom2.JDOMException;
49  import org.mycore.backend.jpa.MCREntityManagerProvider;
50  import org.mycore.common.MCRClassTools;
51  import org.mycore.common.config.MCRConfiguration2;
52  import org.mycore.datamodel.metadata.MCRDerivate;
53  import org.mycore.datamodel.metadata.MCRMetadataManager;
54  import org.mycore.datamodel.metadata.MCRObjectID;
55  import org.mycore.datamodel.niofs.MCRAbstractFileStore;
56  import org.mycore.datamodel.niofs.MCRContentTypes;
57  import org.mycore.datamodel.niofs.MCRPath;
58  import org.mycore.imagetiler.MCRImage;
59  import org.mycore.imagetiler.MCRTiledPictureProps;
60  
61  import jakarta.activation.FileTypeMap;
62  import jakarta.persistence.EntityManager;
63  import jakarta.persistence.TypedQuery;
64  
65  /**
66   * Tools class with common methods for IView2.
67   *
68   * @author Thomas Scheffler (yagee)
69   */
70  public class MCRIView2Tools {
71  
72      public static final String CONFIG_PREFIX = "MCR.Module-iview2.";
73  
74      private static String SUPPORTED_CONTENT_TYPE = MCRConfiguration2.getString(CONFIG_PREFIX + "SupportedContentTypes")
75          .orElse("");
76  
77      private static Path TILE_DIR = Paths.get(MCRIView2Tools.getIView2Property("DirectoryForTiles"));
78  
79      private static Logger LOGGER = LogManager.getLogger(MCRIView2Tools.class);
80  
81      /**
82       * @return directory for tiles
83       */
84      public static Path getTileDir() {
85          return TILE_DIR;
86      }
87  
88      /**
89       * @param derivateID
90       *            ID of derivate
91       * @return empty String or absolute path to main file of derivate if file is supported.
92       */
93      public static String getSupportedMainFile(String derivateID) {
94          try {
95              MCRDerivate deriv = MCRMetadataManager.retrieveMCRDerivate(MCRObjectID.getInstance(derivateID));
96              String nameOfMainFile = deriv.getDerivate().getInternals().getMainDoc();
97              // verify support
98              if (nameOfMainFile != null && !nameOfMainFile.equals("")) {
99                  MCRPath mainFile = MCRPath.getPath(derivateID, '/' + nameOfMainFile);
100                 if (isFileSupported(mainFile)) {
101                     return mainFile.getRoot().relativize(mainFile).toString();
102                 }
103             }
104         } catch (Exception e) {
105             LOGGER.warn("Could not get main file of derivate.", e);
106         }
107         return "";
108     }
109 
110     /**
111      * @param derivateID
112      *            ID of derivate
113      * @return true if {@link #getSupportedMainFile(String)} is not an empty String.
114      */
115     public static boolean isDerivateSupported(String derivateID) {
116         if (derivateID == null || derivateID.trim().length() == 0) {
117             return false;
118         }
119 
120         return getSupportedMainFile(derivateID).length() > 0;
121     }
122 
123     /**
124      * @param file
125      *            image file
126      * @return if content type is in property <code>MCR.Module-iview2.SupportedContentTypes</code>
127      * @see MCRContentTypes#probeContentType(Path)
128      */
129     public static boolean isFileSupported(Path file) throws IOException {
130         try {
131             return Optional.ofNullable(file)
132                 .map(path -> {
133                     try {
134                         return MCRContentTypes.probeContentType(path);
135                     } catch (IOException e) {
136                         throw new UncheckedIOException(e);
137                     }
138                 })
139                 .or(() -> Optional.of("application/octet-stream"))
140                 .map(SUPPORTED_CONTENT_TYPE::contains)
141                 .orElse(Boolean.FALSE);
142         } catch (UncheckedIOException e) {
143             throw e.getCause();
144         }
145     }
146 
147     /**
148      * @return true if the file is supported, false otherwise
149      */
150     public static boolean isFileSupported(String filename) {
151         return SUPPORTED_CONTENT_TYPE.contains(FileTypeMap.getDefaultFileTypeMap().getContentType(
152             filename.toLowerCase(Locale.ROOT)));
153     }
154 
155     /**
156      * Checks for a given derivate id whether all files in that derivate are tiled.
157      *
158      * @return true if all files in belonging to the derivate are tiled, false otherwise
159      */
160     public static boolean isCompletelyTiled(String derivateId) {
161         if (!MCRMetadataManager.exists(MCRObjectID.getInstance(derivateId))) {
162             return false;
163         }
164         EntityManager em = MCREntityManagerProvider.getCurrentEntityManager();
165 
166         TypedQuery<Number> namedQuery = em
167             .createNamedQuery("MCRTileJob.countByStateListByDerivate", Number.class)
168             .setParameter("derivateId", derivateId)
169             .setParameter("states", MCRJobState.notCompleteStates().stream().map(MCRJobState::toChar).collect(
170                 Collectors.toSet()));
171 
172         return namedQuery.getSingleResult().intValue() == 0;
173     }
174 
175     /**
176      * combines image tiles of specified zoomLevel to one image.
177      *
178      * @param iviewFile
179      *            .iview2 file
180      * @param zoomLevel
181      *            the zoom level where 0 is thumbnail size
182      * @return a combined image
183      * @throws IOException
184      *             any IOException while reading tiles
185      * @throws JDOMException
186      *             if image properties could not be parsed.
187      */
188     public static BufferedImage getZoomLevel(Path iviewFile, int zoomLevel) throws IOException, JDOMException {
189         ImageReader reader = getTileImageReader();
190         try (FileSystem zipFileSystem = getFileSystem(iviewFile)) {
191             Path iviewFileRoot = zipFileSystem.getRootDirectories().iterator().next();
192             MCRTiledPictureProps imageProps = MCRTiledPictureProps.getInstanceFromDirectory(iviewFileRoot);
193             if (zoomLevel < 0 || zoomLevel > imageProps.getZoomlevel()) {
194                 throw new IndexOutOfBoundsException(
195                     "Zoom level " + zoomLevel + " is not in range 0 - " + imageProps.getZoomlevel());
196             }
197             return getZoomLevel(iviewFileRoot, imageProps, reader, zoomLevel);
198         } finally {
199             reader.dispose();
200         }
201     }
202 
203     /**
204      * combines image tiles of specified zoomLevel to one image.
205      *
206      * @param iviewFileRoot
207      *            root directory of .iview2 file
208      * @param imageProperties
209      *            imageProperties, if available or null
210      * @param zoomLevel
211      *            the zoom level where 0 is thumbnail size
212      * @return a combined image
213      * @throws IOException
214      *             any IOException while reading tiles
215      * @throws JDOMException
216      *             if image properties could not be parsed.
217      */
218     public static BufferedImage getZoomLevel(final Path iviewFileRoot, final MCRTiledPictureProps imageProperties,
219         final ImageReader reader, final int zoomLevel) throws IOException, JDOMException {
220         if (zoomLevel == 0) {
221             return readTile(iviewFileRoot, reader, 0, 0, 0);
222         }
223         MCRTiledPictureProps imageProps = imageProperties == null
224             ? MCRTiledPictureProps.getInstanceFromDirectory(iviewFileRoot)
225             : imageProperties;
226         double zoomFactor = Math.pow(2, (imageProps.getZoomlevel() - zoomLevel));
227         int maxX = (int) Math.ceil((imageProps.getWidth() / zoomFactor) / MCRImage.getTileSize());
228         int maxY = (int) Math.ceil((imageProps.getHeight() / zoomFactor) / MCRImage.getTileSize());
229         LOGGER.debug("Image size:{}x{}, tiles:{}x{}", imageProps.getWidth(), imageProps.getHeight(), maxX, maxY);
230         int imageType = getImageType(iviewFileRoot, reader, zoomLevel, 0, 0);
231         int xDim = ((maxX - 1) * MCRImage.getTileSize()
232             + readTile(iviewFileRoot, reader, zoomLevel, maxX - 1, 0).getWidth());
233         int yDim = ((maxY - 1) * MCRImage.getTileSize()
234             + readTile(iviewFileRoot, reader, zoomLevel, 0, maxY - 1).getHeight());
235         BufferedImage resultImage = new BufferedImage(xDim, yDim, imageType);
236         Graphics graphics = resultImage.getGraphics();
237         try {
238             for (int x = 0; x < maxX; x++) {
239                 for (int y = 0; y < maxY; y++) {
240                     BufferedImage tile = readTile(iviewFileRoot, reader, zoomLevel, x, y);
241                     graphics.drawImage(tile, x * MCRImage.getTileSize(), y * MCRImage.getTileSize(), null);
242                 }
243             }
244             return resultImage;
245         } finally {
246             graphics.dispose();
247         }
248     }
249 
250     public static FileSystem getFileSystem(Path iviewFile) throws IOException {
251         URI uri = URI.create("jar:" + iviewFile.toUri());
252         try {
253             return FileSystems.newFileSystem(uri, Collections.emptyMap(), MCRClassTools.getClassLoader());
254         } catch (FileSystemAlreadyExistsException exc) {
255             // block until file system is closed
256             try {
257                 FileSystem fileSystem = FileSystems.getFileSystem(uri);
258                 while (fileSystem.isOpen()) {
259                     try {
260                         Thread.sleep(10);
261                     } catch (InterruptedException ie) {
262                         // get out of here
263                         throw new IOException(ie);
264                     }
265                 }
266             } catch (FileSystemNotFoundException fsnfe) {
267                 // seems closed now -> do nothing and try to return the file system again
268                 LOGGER.debug("Filesystem not found", fsnfe);
269             }
270             return getFileSystem(iviewFile);
271         }
272     }
273 
274     public static ImageReader getTileImageReader() {
275         return ImageIO.getImageReadersByMIMEType("image/jpeg").next();
276     }
277 
278     public static BufferedImage readTile(Path iviewFileRoot, ImageReader imageReader, int zoomLevel, int x, int y)
279         throws IOException {
280         String tileName = new MessageFormat("{0}/{1}/{2}.jpg", Locale.ROOT).format(new Object[] { zoomLevel, y, x });
281         Path tile = iviewFileRoot.resolve(tileName);
282         if (Files.exists(tile)) {
283             try (SeekableByteChannel fileChannel = Files.newByteChannel(tile)) {
284                 ImageInputStream iis = ImageIO.createImageInputStream(fileChannel);
285                 if (iis == null) {
286                     throw new IOException("Could not acquire ImageInputStream from SeekableByteChannel: " + tile);
287                 }
288                 imageReader.setInput(iis, true);
289                 BufferedImage image = imageReader.read(0);
290                 imageReader.reset();
291                 iis.close();
292                 return image;
293             }
294         } else {
295             throw new NoSuchFileException(iviewFileRoot.toString(), tileName, null);
296         }
297     }
298 
299     public static int getImageType(Path iviewFileRoot, ImageReader imageReader, int zoomLevel, int x, int y)
300         throws IOException {
301         String tileName = new MessageFormat("{0}/{1}/{2}.jpg", Locale.ROOT).format(new Object[] { zoomLevel, y, x });
302         Path tile = iviewFileRoot.resolve(tileName);
303         if (Files.exists(tile)) {
304             try (SeekableByteChannel fileChannel = Files.newByteChannel(tile)) {
305                 ImageInputStream iis = ImageIO.createImageInputStream(fileChannel);
306                 if (iis == null) {
307                     throw new IOException("Could not acquire ImageInputStream from SeekableByteChannel: " + tile);
308                 }
309                 imageReader.setInput(iis, true);
310                 int imageType = MCRImage.getImageType(imageReader);
311                 imageReader.reset();
312                 iis.close();
313                 return imageType;
314             }
315         } else {
316             throw new NoSuchFileException(iviewFileRoot.toString(), tileName, null);
317         }
318     }
319 
320     /**
321      * short for {@link MCRIView2Tools}{@link #getIView2Property(String, String)} defaultProp = null
322      */
323     public static String getIView2Property(String propName) {
324         return getIView2Property(propName, null);
325     }
326 
327     /**
328      * short for <code>MCRConfiguration2.getString("MCR.Module-iview2." + propName).orElse(defaultProp);</code>
329      *
330      * @param propName
331      *            any suffix
332      * @return null or property value
333      */
334     public static String getIView2Property(String propName, String defaultProp) {
335         return MCRConfiguration2.getString(CONFIG_PREFIX + propName).orElse(defaultProp);
336     }
337 
338     public static String getFilePath(String derID, String derPath) throws IOException {
339         MCRPath mcrPath = MCRPath.getPath(derID, derPath);
340         Path physicalPath = mcrPath.toPhysicalPath();
341         for (FileStore fs : mcrPath.getFileSystem().getFileStores()) {
342             if (fs instanceof MCRAbstractFileStore) {
343                 Path basePath = ((MCRAbstractFileStore) fs).getBaseDirectory();
344                 if (physicalPath.startsWith(basePath)) {
345                     return basePath.relativize(physicalPath).toString();
346                 }
347             }
348         }
349         return physicalPath.toString();
350     }
351 }