001/* 002 * Copyright 2019 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.util.cocoon; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.OutputStream; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Map; 024import java.util.Set; 025 026import org.apache.avalon.framework.parameters.Parameters; 027import org.apache.cocoon.ProcessingException; 028import org.apache.cocoon.environment.ObjectModelHelper; 029import org.apache.cocoon.environment.Response; 030import org.apache.cocoon.environment.SourceResolver; 031import org.apache.cocoon.reading.AbstractReader; 032import org.apache.commons.io.IOUtils; 033import org.xml.sax.SAXException; 034 035import org.ametys.core.util.ImageHelper; 036import org.ametys.core.util.URIUtils; 037 038/** 039 * Abstract reader for Ametys resources. 040 * If the resource is an image, this reader handles resizing and cropping. 041 */ 042public abstract class AbstractResourceReader extends AbstractReader 043{ 044 /** Default returned format */ 045 protected static final String __DEFAULT_FORMAT = "png"; 046 047 /** List of allowed output formats */ 048 protected static final Collection<String> __ALLOWED_OUTPUT_FORMATS = Arrays.asList("png", "gif", "jpg", "jpeg"); 049 /** List of format that cannot be resized */ 050 protected static final Collection<String> __UNRESIZABLE_FORMATS = Set.of("svg"); 051 052 private int _width; 053 private int _height; 054 private int _maxWidth; 055 private int _maxHeight; 056 private int _cropWidth; 057 private int _cropHeight; 058 059 private boolean _readForDownload; 060 061 @Override 062 public void setup(SourceResolver sresolver, Map sObjectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException 063 { 064 super.setup(sresolver, sObjectModel, src, par); 065 066 doSetup(sresolver, sObjectModel, src, par); 067 068 _readForDownload = par.getParameterAsBoolean("download", false); 069 Response response = ObjectModelHelper.getResponse(sObjectModel); 070 071 if (_readForDownload) 072 { 073 String name = getFilename(); 074 String encodedName = getEncodedFilename(); 075 encodedName = encodedName != null ? URIUtils.encodeHeader(encodedName) : null; 076 077 response.setHeader("Content-Disposition", "attachment; filename=\"" + (encodedName != null ? encodedName : name) + "\"" + (encodedName != null ? ";filename*=UTF-8''" + encodedName : "")); 078 } 079 080 // parameters for image resizing 081 _width = par.getParameterAsInteger("width", 0); 082 _height = par.getParameterAsInteger("height", 0); 083 _maxWidth = par.getParameterAsInteger("maxWidth", 0); 084 _maxHeight = par.getParameterAsInteger("maxHeight", 0); 085 _cropWidth = par.getParameterAsInteger("cropWidth", 0); 086 _cropHeight = par.getParameterAsInteger("cropHeight", 0); 087 } 088 089 /** 090 * Called by {@link #setup(SourceResolver, Map, String, Parameters)}. This method should be implemented by subclasses to retrieve the actual resource. 091 * @param res the {@link SourceResolver}. 092 * @param objModel the Cocoon's object model. 093 * @param src the source, as given by the sitemap. 094 * @param par the parameters, as given by the sitemap. 095 * @throws ProcessingException if an error occurs while processing the resource. 096 * @throws IOException if an error occurs while accessing the resource. 097 */ 098 protected abstract void doSetup(SourceResolver res, Map objModel, String src, Parameters par) throws ProcessingException, IOException; 099 100 @SuppressWarnings("deprecation") 101 public void generate() throws IOException, SAXException, ProcessingException 102 { 103 Response response = ObjectModelHelper.getResponse(objectModel); 104 105 try (InputStream is = getInputStream()) 106 { 107 String fileName = getFilename(); 108 int i = fileName.lastIndexOf('.'); 109 String fileExtension = i != -1 ? fileName.substring(i + 1).toLowerCase() : __DEFAULT_FORMAT; 110 111 if (processImage(fileExtension)) 112 { 113 String outputFormat = __ALLOWED_OUTPUT_FORMATS.contains(fileExtension) ? fileExtension : __DEFAULT_FORMAT; 114 115 generateThumbnail(is, outputFormat); 116 117 if (getLogger().isDebugEnabled()) 118 { 119 getLogger().debug("Image generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI()); 120 } 121 } 122 else 123 { 124 // Copy data in response 125 response.setHeader("Content-Length", Long.toString(getLength())); 126 IOUtils.copy(is, out); 127 128 if (getLogger().isDebugEnabled()) 129 { 130 getLogger().debug("Resource generated: " + ObjectModelHelper.getRequest(objectModel).getRequestURI()); 131 } 132 } 133 } 134 finally 135 { 136 IOUtils.closeQuietly(out); 137 } 138 } 139 140 /** 141 * Generate the thumbnail in {@link OutputStream} out 142 * @param is original image 143 * @param format format of the file 144 * @throws IOException if an error occurs when manipulating streams. 145 */ 146 protected void generateThumbnail(InputStream is, String format) throws IOException 147 { 148 ImageHelper.generateThumbnail(is, out, format, _height, _width, _maxHeight, _maxWidth, _cropHeight, _cropWidth); 149 } 150 151 /** 152 * Returns the resource's {@link InputStream}. 153 * @return the resource's {@link InputStream}. 154 */ 155 protected abstract InputStream getInputStream(); 156 157 /** 158 * Returns the resource's name. 159 * @return the resource's name. 160 */ 161 protected abstract String getFilename(); 162 163 /** 164 * If needed, returns the resource's name, properly encoded for using in a "Content-Disposition" HTTP header.<br> 165 * May be null, in which case the result of {@link #getFilename()} is used instead. 166 * @return the encoded resource's name, if any. 167 */ 168 protected abstract String getEncodedFilename(); 169 170 /** 171 * Returns the resource's length. 172 * @return the resource's length. 173 */ 174 protected abstract long getLength(); 175 176 177 /** 178 * Determines if the file is an image and should be processed. 179 * @param fileExtension The file extension to process 180 * @return <code>true</code> if file is a image 181 */ 182 protected boolean processImage(String fileExtension) 183 { 184 if (__UNRESIZABLE_FORMATS.contains(fileExtension)) 185 { 186 return false; 187 } 188 else if (_width > 0 || _height > 0 || _maxHeight > 0 || _maxWidth > 0 || _cropHeight > 0 || _cropWidth > 0) 189 { 190 // resize or crop is required, assume this is an image 191 return true; 192 } 193 else if (!_readForDownload) 194 { 195 // only process image if it is for rendering purposes 196 return getMimeType() != null && getMimeType().startsWith("image/"); 197 } 198 else 199 { 200 return false; 201 } 202 } 203 204 /** 205 * Helper method to compute a suffix based on resizing and cropping properties. 206 * @return a String to be used in cache keys. 207 */ 208 protected String getKeySuffix() 209 { 210 return "#" + _height + "#" + _width + "#" + _maxHeight + "#" + _maxWidth + "#" + _cropHeight + "#" + _cropWidth; 211 } 212}