001/*
002 *  Copyright 2011 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.skinfactory.parameters;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.nio.file.attribute.FileTime;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030
031import org.apache.cocoon.xml.AttributesImpl;
032import org.apache.cocoon.xml.XMLUtils;
033import org.apache.commons.io.IOUtils;
034import org.xml.sax.ContentHandler;
035import org.xml.sax.SAXException;
036
037import org.ametys.runtime.i18n.I18nizableText;
038import org.ametys.web.skin.SkinModel;
039
040/**
041 * Implementation of {@link AbstractSkinParameter} for an image
042 */
043public class ImageParameter extends AbstractSkinParameter
044{
045    private String _iconGlyph;
046    private String _iconSmall;
047    private String _iconLarge;
048    private boolean _localUploadEnabled;
049    private String _imagePath;
050    
051    /**
052     * Constructor
053     * @param relPath the relative path of the target image
054     * @param label the label
055     * @param description the description
056     */
057    public ImageParameter(String relPath, I18nizableText label, I18nizableText description)
058    {
059        super(relPath.replaceAll("\\\\", "/"), label, description);
060        _imagePath = relPath.replaceAll("\\\\", "/");
061        _localUploadEnabled = false;
062    }
063    
064    /**
065     * Constructor
066     * @param relPath the relative path of the target image
067     * @param label the label
068     * @param description the description
069     * @param iconGlyph The CSS classe for icon, to use instead of small and large icon
070     * @param iconSmall The small icon
071     * @param iconLarge The large icon
072     */
073    public ImageParameter(String relPath, I18nizableText label, I18nizableText description, String iconGlyph, String iconSmall, String iconLarge)
074    {
075        super(relPath.replaceAll("\\\\", "/"), label, description);
076        _imagePath = relPath.replaceAll("\\\\", "/");
077        _localUploadEnabled = false;
078        _iconGlyph = iconGlyph;
079        _iconLarge = iconLarge;
080        _iconSmall = iconSmall;
081    }
082    
083    @Override
084    public SkinParameterType getType()
085    {
086        return SkinParameterType.IMAGE;
087    }
088    
089    /**
090     * Determines if local upload is enabled
091     * @return true if local upload is enabled
092     */
093    public boolean isLocalUploadEnabled ()
094    {
095        return _localUploadEnabled;
096    }
097    
098    /**
099     * Get relative path of images
100     * @return the relative path of images
101     */
102    public String getLibraryPath ()
103    {
104        return this._imagePath;
105    }
106    
107    /**
108     * Set the CSS icon
109     * @param iconGlyph the CSS icon
110     */
111    public void setIconGlyph(String iconGlyph)
112    {
113        _iconGlyph = iconGlyph;
114    }
115    
116    /**
117     * Get the CSS icon
118     * @return The CSS icon
119     */
120    public String getIconGlyph ()
121    {
122        return _iconGlyph;
123    }
124    
125    /**
126     * Set the small icon relative path
127     * @param iconSmall the relative path of the small icon
128     */
129    public void setIconSmall(String iconSmall)
130    {
131        _iconSmall = iconSmall;
132    }
133    
134    /**
135     * Get the small icon
136     * @return The small icon
137     */
138    public String getIconSmall ()
139    {
140        return _iconSmall;
141    }
142    
143    /**
144     * Set the large icon relative path
145     * @param iconLarge the relative path of the large icon
146     */
147    public void setIconLarge(String iconLarge)
148    {
149        _iconLarge = iconLarge;
150    }
151    
152    /**
153     * Get the large icon
154     * @return The large icon
155     */
156    public String getIconLarge ()
157    {
158        return _iconLarge;
159    }
160    
161    @Override
162    public void apply(Path tempDir, Path modelDir, Object value, String lang)
163    {
164        Path targetFile = tempDir.resolve("resources/img/" + this._imagePath);
165        
166        boolean uploaded = value instanceof FileValue ? ((FileValue) value).isUploaded() : false;
167        Path libraryFile = _getLibraryFile(tempDir, modelDir, uploaded);
168        
169        String filePath = value instanceof FileValue ? ((FileValue) value).getPath() : (String) value;
170        Path srcFile = libraryFile.resolve(filePath);
171        
172        _copyFile(srcFile, targetFile);
173        
174        Path resourcesFile = tempDir.resolve("resources");
175        try
176        {
177            // Update css file last modified date to avoid cache issue in case of background image
178            for (Path file : _listCSSFiles(resourcesFile))
179            {
180                Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));
181            }
182        }
183        catch (IOException e)
184        {
185            throw new RuntimeException("An error occurred while retrieving CSS files in " + resourcesFile.toString());
186        }
187    }
188    
189    private List<Path> _listCSSFiles(Path file) throws IOException
190    {
191        return Files.walk(file)
192                    .filter(Files::isRegularFile)
193                    .filter(f -> f.getFileName().toString().endsWith(".css"))
194                    .collect(Collectors.toList());
195    }
196    private Path _getLibraryFile(Path tempDir, Path modelDir, boolean uploaded)
197    {
198        if (uploaded)
199        {
200            return tempDir.resolve("model/_uploads/" + this._imagePath);
201        }
202        else
203        {
204            return modelDir.resolve("model/images/" + this._imagePath);
205        }
206    }
207    
208    private void _copyFile(Path srcFile, Path targetFile)
209    {
210        if (!Files.exists(srcFile))
211        {
212            return;
213        }
214        
215        try
216        {
217            Files.createDirectories(targetFile.getParent());
218         
219            try (InputStream is = Files.newInputStream(srcFile); OutputStream os = Files.newOutputStream(targetFile))
220            {
221                IOUtils.copy(is, os);
222                
223                Files.setLastModifiedTime(targetFile, FileTime.fromMillis(System.currentTimeMillis()));
224            }
225        }
226        catch (IOException e)
227        {
228            throw new SkinParameterException ("Unable to apply image parameter '" + getId() + "'", e);
229        }
230    }
231    
232    @Override
233    public void toSAX(ContentHandler contentHandler, String modelName) throws SAXException
234    {
235        AttributesImpl attrs = new AttributesImpl();
236        attrs.addCDATAAttribute("id", getId());
237        attrs.addCDATAAttribute("type", SkinParameterType.IMAGE.name().toLowerCase());
238        
239        XMLUtils.startElement(contentHandler, "parameter", attrs);
240        
241        getLabel().toSAX(contentHandler, "label");
242        getDescription().toSAX(contentHandler, "description");
243        XMLUtils.createElement(contentHandler, "path", this._imagePath);
244        
245        if (getIconGlyph() != null)
246        {
247            XMLUtils.createElement(contentHandler, "iconGlyph", getIconGlyph());
248        }
249        
250        if (getIconSmall() != null)
251        {
252            XMLUtils.createElement(contentHandler, "iconSmall", "/plugins/skinfactory/" + modelName + "/_thumbnail/16/16/model/images/" + this._imagePath + "/" + getIconSmall());
253        }
254        else
255        {
256            XMLUtils.createElement(contentHandler, "iconSmall", "/plugins/skinfactory/resources/img/button/image_16.png");
257        }
258        
259        if (getIconLarge() != null)
260        {
261            XMLUtils.createElement(contentHandler, "iconLarge", "/plugins/skinfactory/" + modelName + "/_thumbnail/32/32/model/images/" + this._imagePath + "/" + getIconLarge());
262        }
263        else
264        {
265            XMLUtils.createElement(contentHandler, "iconLarge", "/plugins/skinfactory/resources/img/button/image_32.png");
266        }
267        
268        XMLUtils.endElement(contentHandler, "parameter");
269    }
270    
271    @Override
272    public Map<String, Object> toJson(String modelName)
273    {
274        Map<String, Object> jsonObject = new HashMap<>();
275        
276        jsonObject.put("id", getId());
277        jsonObject.put("type", SkinParameterType.IMAGE.name().toLowerCase());
278        
279        jsonObject.put("label", getLabel());
280        jsonObject.put("description", getDescription());
281        jsonObject.put("path", this._imagePath);
282        
283        if (getIconGlyph() != null)
284        {
285            jsonObject.put("iconGlyph", getIconGlyph());
286        }
287        
288        if (getIconSmall() != null)
289        {
290            jsonObject.put("iconSmall", "/plugins/skinfactory/" + modelName + "/_thumbnail/16/16/model/images/" + this._imagePath + "/" + getIconSmall());
291        }
292        else
293        {
294            jsonObject.put("iconSmall", "/plugins/skinfactory/resources/img/button/image_16.png");
295        }
296        
297        if (getIconLarge() != null)
298        {
299            jsonObject.put("iconLarge", "/plugins/skinfactory/" + modelName + "/_thumbnail/32/32/model/images/" + this._imagePath + "/" + getIconLarge());
300        }
301        else
302        {
303            jsonObject.put("iconLarge", "/plugins/skinfactory/resources/img/button/image_32.png");
304        }
305        
306        return jsonObject;
307    }
308    
309    
310    @Override
311    public FileValue getDefaultValue(SkinModel model)
312    {
313        Path libraryFile = model.getPath().resolve("model/images/" + this._imagePath);
314        
315        try (Stream<Path> s = Files.list(libraryFile))
316        {
317            List<Path> files = s.collect(Collectors.toList());
318            for (Path file : files)
319            {
320                if (Files.isDirectory(file))
321                {
322                    try (Stream<Path> s2 = Files.list(file))
323                    {
324                        List<Path> subfiles = s2.collect(Collectors.toList());
325                        for (Path child : subfiles)
326                        {
327                            if (_isImage(child))
328                            {
329                                String defaultPath = libraryFile.relativize(child).toString();
330                                defaultPath.replaceAll(File.separator.equals("\\") ? File.separator + File.separator : File.separator, ".");
331                                
332                                return new FileValue(defaultPath, false);
333                            }
334                        }
335                    }
336                    
337                }
338                else if (_isImage(file))
339                {
340                    String defaultPath = libraryFile.relativize(file).toString();
341                    defaultPath.replaceAll(File.separator.equals("\\") ? File.separator + File.separator : File.separator, ".");
342                    
343                    return new FileValue(defaultPath, false);
344                }
345            }
346        }
347        catch (IOException e)
348        {
349            throw new RuntimeException("Cannot get default value for model " + model.getId(), e);
350        }
351        
352        return null;
353    }
354    
355    private boolean _isImage(Path file)
356    {
357        if (Files.isDirectory(file))
358        {
359            return false;
360        }
361        
362        String name = file.getFileName().toString().toLowerCase();
363        int index = name.lastIndexOf(".");
364        String ext = name.substring(index + 1);
365        
366        if (name.equals("thumbnail_16.png") || name.equals("thumbnail_32.png") || name.equals("thumbnail_48.png"))
367        {
368            return false;
369        }
370
371        return "png".equals(ext) || "gif".equals(ext) || "jpg".equals(ext) || "jpeg".equals(ext);
372    }
373    
374    @Override
375    public FileValue getDefaultValue(SkinModel model, String lang)
376    {
377        return getDefaultValue(model);
378    }
379    
380    /**
381     * Class representing a file value
382     *
383     */
384    public static class FileValue 
385    {
386        private String _path;
387        private boolean _uploaded;
388        
389        /**
390         * Constructor
391         * @param path The relative file path
392         * @param uploaded <code>true</code> if the file was uploaded
393         */
394        public FileValue(String path, boolean uploaded)
395        {
396            _uploaded = uploaded;
397            _path = path;
398        }
399        
400        /**
401         * Determines if the file was uploaded
402         * @return <code>true</code> if the file was uploaded
403         */
404        public boolean isUploaded ()
405        {
406            return _uploaded;
407        }
408        
409        /**
410         * Get the relative file path
411         * @return the relative file path
412         */
413        public String getPath ()
414        {
415            return _path;
416        }
417    }
418
419}