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.services.zipper;
20  
21  import static org.mycore.access.MCRAccessManager.PERMISSION_READ;
22  import static org.mycore.common.MCRConstants.XLINK_NAMESPACE;
23  
24  import java.io.ByteArrayOutputStream;
25  import java.io.FileNotFoundException;
26  import java.io.IOException;
27  import java.nio.file.FileVisitResult;
28  import java.nio.file.Files;
29  import java.nio.file.NoSuchFileException;
30  import java.nio.file.Path;
31  import java.nio.file.SimpleFileVisitor;
32  import java.nio.file.attribute.BasicFileAttributes;
33  import java.text.MessageFormat;
34  import java.util.Date;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  
40  import org.apache.logging.log4j.LogManager;
41  import org.apache.logging.log4j.Logger;
42  import org.jdom2.Element;
43  import org.mycore.access.MCRAccessManager;
44  import org.mycore.common.MCRException;
45  import org.mycore.common.content.MCRContent;
46  import org.mycore.common.content.transformer.MCRContentTransformer;
47  import org.mycore.common.content.transformer.MCRParameterizedTransformer;
48  import org.mycore.common.xml.MCRLayoutService;
49  import org.mycore.common.xml.MCRXMLFunctions;
50  import org.mycore.common.xsl.MCRParameterCollector;
51  import org.mycore.datamodel.common.MCRISO8601Date;
52  import org.mycore.datamodel.common.MCRXMLMetadataManager;
53  import org.mycore.datamodel.metadata.MCRObjectID;
54  import org.mycore.datamodel.niofs.MCRPath;
55  import org.mycore.frontend.servlets.MCRServlet;
56  import org.mycore.frontend.servlets.MCRServletJob;
57  
58  import jakarta.servlet.ServletOutputStream;
59  import jakarta.servlet.http.HttpServletRequest;
60  import jakarta.servlet.http.HttpServletResponse;
61  
62  /**
63   * This servlet delivers the contents of MCROjects to the client in
64   * container files (see classes extending this servlet). Read permission is required.
65   * There are three modes
66   * <ol>
67   *  <li>if id=mycoreobjectID (delivers the metadata, including all derivates)</li>
68   *  <li>if id=derivateID (delivers all files of the derivate)</li>
69   *  <li>if id=derivateID/directoryPath (delivers all files in the given directory of the derivate)</li>
70   * </ol>
71   * 
72   * "id" maybe specified as {@link HttpServletRequest#getPathInfo()} or as
73   * {@link MCRServlet#getProperty(HttpServletRequest, String)}.
74   * @author Thomas Scheffler (yagee)
75   *
76   */
77  public abstract class MCRCompressServlet<T extends AutoCloseable> extends MCRServlet {
78      private static final long serialVersionUID = 1L;
79  
80      protected static String KEY_OBJECT_ID = MCRCompressServlet.class.getCanonicalName() + ".object";
81  
82      protected static String KEY_PATH = MCRCompressServlet.class.getCanonicalName() + ".path";
83  
84      private static Pattern PATH_INFO_PATTERN = Pattern.compile("\\A([\\w]+)/([\\w/]+)\\z");
85  
86      private static Logger LOGGER = LogManager.getLogger(MCRCompressServlet.class);
87  
88      @Override
89      protected void think(MCRServletJob job) throws Exception {
90          HttpServletRequest req = job.getRequest();
91          //id parameter for backward compatibility
92          String paramid = getProperty(req, "id");
93          if (paramid == null) {
94              String pathInfo = req.getPathInfo();
95              if (pathInfo != null) {
96                  paramid = pathInfo.substring(1);
97              }
98          }
99          if (paramid == null) {
100             job.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST, "What should I do?");
101             return;
102         }
103         Matcher ma = PATH_INFO_PATTERN.matcher(paramid);
104         //path & directory
105         MCRObjectID id;
106         String path;
107         try {
108             if (ma.find()) {
109                 id = MCRObjectID.getInstance(ma.group(1));
110                 path = ma.group(2);
111             } else {
112                 id = MCRObjectID.getInstance(paramid);
113                 path = null;
114             }
115         } catch (MCRException e) {
116             String objId = ma.find() ? ma.group(1) : paramid;
117             job.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST, "ID is not valid: " + objId);
118             return;
119         }
120         boolean readPermission = id.getTypeId().equals("derivate") ? MCRAccessManager
121             .checkDerivateContentPermission(id, PERMISSION_READ)
122             : MCRAccessManager.checkPermission(id, PERMISSION_READ);
123         if (!readPermission) {
124             job.getResponse().sendError(HttpServletResponse.SC_FORBIDDEN, "You may not read " + id);
125             return;
126         }
127         req.setAttribute(KEY_OBJECT_ID, id);
128         req.setAttribute(KEY_PATH, path);
129     }
130 
131     @Override
132     protected void render(MCRServletJob job, Exception ex) throws Exception {
133         if (ex != null) {
134             //we cannot handle it ourself
135             throw ex;
136         }
137         if (job.getResponse().isCommitted()) {
138             return;
139         }
140         MCRObjectID id = (MCRObjectID) job.getRequest().getAttribute(KEY_OBJECT_ID);
141         String path = (String) job.getRequest().getAttribute(KEY_PATH);
142         try (ServletOutputStream sout = job.getResponse().getOutputStream()) {
143             StringBuffer requestURL = job.getRequest().getRequestURL();
144             if (job.getRequest().getQueryString() != null) {
145                 requestURL.append('?').append(job.getRequest().getQueryString());
146             }
147             MCRISO8601Date mcriso8601Date = new MCRISO8601Date();
148             mcriso8601Date.setDate(new Date());
149             String comment = "Created by " + requestURL + " at " + mcriso8601Date.getISOString();
150             try (T container = createContainer(sout, comment)) {
151                 job.getResponse().setContentType(getMimeType());
152                 String filename = getFileName(id, path);
153                 job.getResponse().addHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
154                 if (id.getTypeId().equals("derivate")) {
155                     sendDerivate(id, path, container);
156                 } else {
157                     sendObject(id, job, container);
158                 }
159                 disposeContainer(container);
160             }
161         }
162     }
163 
164     private void sendObject(MCRObjectID id, MCRServletJob job, T container) throws Exception {
165         MCRContent content = MCRXMLMetadataManager.instance().retrieveContent(id);
166         if (content == null) {
167             throw new FileNotFoundException("Could not find object: " + id);
168         }
169         long lastModified = MCRXMLMetadataManager.instance().getLastModified(id);
170         HttpServletRequest req = job.getRequest();
171         byte[] metaDataContent = getMetaDataContent(content, req);
172         sendMetadataCompressed("metadata.xml", metaDataContent, lastModified, container);
173 
174         // zip all derivates
175         List<Element> li = content.asXML().getRootElement().getChild("structure").getChild("derobjects")
176             .getChildren("derobject");
177 
178         for (Element el : li) {
179             if (el.getAttributeValue("inherited").equals("0")) {
180                 String ownerID = el.getAttributeValue("href", XLINK_NAMESPACE);
181                 MCRObjectID derId = MCRObjectID.getInstance(ownerID);
182                 // here the access check is tested only against the derivate
183                 if (MCRAccessManager.checkDerivateContentPermission(derId, PERMISSION_READ)
184                     && MCRXMLFunctions.isDisplayedEnabledDerivate(ownerID)) {
185                     sendDerivate(derId, null, container);
186                 }
187             }
188         }
189     }
190 
191     private byte[] getMetaDataContent(MCRContent content, HttpServletRequest req) throws Exception {
192         // zip the object's Metadata
193         MCRParameterCollector parameters = new MCRParameterCollector(req);
194         if (parameters.getParameter("Style", null) == null) {
195             parameters.setParameter("Style", "compress");
196         }
197         MCRContentTransformer contentTransformer = MCRLayoutService.getContentTransformer(content.getDocType(),
198             parameters);
199         ByteArrayOutputStream out = new ByteArrayOutputStream(32 * 1024);
200         if (contentTransformer instanceof MCRParameterizedTransformer) {
201             ((MCRParameterizedTransformer) contentTransformer).transform(content, out, parameters);
202         } else {
203             contentTransformer.transform(content, out);
204         }
205         return out.toByteArray();
206     }
207 
208     private void sendDerivate(MCRObjectID id, String path, T container) throws IOException {
209 
210         MCRPath resolvedPath = MCRPath.getPath(id.toString(), path == null ? "/" : path);
211 
212         if (!Files.exists(resolvedPath)) {
213             throw new NoSuchFileException(id.toString(), path, "Could not find path " + resolvedPath);
214         }
215 
216         if (Files.isRegularFile(resolvedPath)) {
217             BasicFileAttributes attrs = Files.readAttributes(resolvedPath, BasicFileAttributes.class);
218             sendCompressedFile(resolvedPath, attrs, container);
219             LOGGER.debug("file {} zipped", resolvedPath);
220             return;
221         }
222         // root is a directory
223         Files.walkFileTree(resolvedPath, new CompressVisitor<>(this, container));
224     }
225 
226     private String getFileName(MCRObjectID id, String path) {
227         if (path == null || path.equals("")) {
228             return new MessageFormat("{0}.{1}", Locale.ROOT).format(new Object[] { id, getFileExtension() });
229         } else {
230             return new MessageFormat("{0}-{1}.{2}", Locale.ROOT).format(
231                 new Object[] { id, path.replaceAll("/", "-"), getFileExtension() });
232         }
233     }
234 
235     /**
236      * Constructs a path name in form of {ownerID}+'/'+{path} or {ownerID} if path is root component.
237      * @param path absolute path
238      */
239     protected String getFilename(MCRPath path) {
240         return path.getNameCount() == 0 ? path.getOwner()
241             : path.getOwner() + '/'
242                 + path.getRoot().relativize(path);
243     }
244 
245     protected abstract void sendCompressedDirectory(MCRPath file, BasicFileAttributes attrs, T container)
246         throws IOException;
247 
248     protected abstract void sendCompressedFile(MCRPath file, BasicFileAttributes attrs, T container) throws IOException;
249 
250     protected abstract void sendMetadataCompressed(String fileName, byte[] content, long modified, T container)
251         throws IOException;
252 
253     protected abstract String getMimeType();
254 
255     protected abstract String getFileExtension();
256 
257     protected abstract T createContainer(ServletOutputStream sout, String comment);
258 
259     protected abstract void disposeContainer(T container) throws IOException;
260 
261     private static class CompressVisitor<T extends AutoCloseable> extends SimpleFileVisitor<Path> {
262 
263         private MCRCompressServlet<T> impl;
264 
265         private T container;
266 
267         CompressVisitor(MCRCompressServlet<T> impl, T container) {
268             this.impl = impl;
269             this.container = container;
270         }
271 
272         @Override
273         public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
274             impl.sendCompressedDirectory(MCRPath.toMCRPath(dir), attrs, container);
275             return super.preVisitDirectory(dir, attrs);
276         }
277 
278         @Override
279         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
280             impl.sendCompressedFile(MCRPath.toMCRPath(file), attrs, container);
281             LOGGER.debug("file {} zipped", file);
282             return super.visitFile(file, attrs);
283         }
284 
285     }
286 
287 }