001/*
002 *  Copyright 2015 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.web.skin;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029import java.util.Set;
030
031import javax.xml.transform.OutputKeys;
032import javax.xml.transform.TransformerConfigurationException;
033import javax.xml.transform.TransformerFactory;
034import javax.xml.transform.sax.SAXTransformerFactory;
035import javax.xml.transform.sax.TransformerHandler;
036import javax.xml.transform.stream.StreamResult;
037import javax.xml.xpath.XPath;
038import javax.xml.xpath.XPathExpressionException;
039import javax.xml.xpath.XPathFactory;
040
041import org.apache.avalon.framework.component.Component;
042import org.apache.avalon.framework.context.Context;
043import org.apache.avalon.framework.context.ContextException;
044import org.apache.avalon.framework.context.Contextualizable;
045import org.apache.avalon.framework.logger.AbstractLogEnabled;
046import org.apache.avalon.framework.service.ServiceException;
047import org.apache.avalon.framework.service.ServiceManager;
048import org.apache.avalon.framework.service.Serviceable;
049import org.apache.avalon.framework.thread.ThreadSafe;
050import org.apache.cocoon.Constants;
051import org.apache.cocoon.ProcessingException;
052import org.apache.cocoon.util.HashUtil;
053import org.apache.cocoon.xml.XMLUtils;
054import org.apache.commons.io.FileUtils;
055import org.apache.commons.io.filefilter.FileFileFilter;
056import org.apache.commons.io.filefilter.TrueFileFilter;
057import org.apache.excalibur.source.SourceResolver;
058import org.xml.sax.InputSource;
059import org.xml.sax.SAXException;
060import org.xml.sax.helpers.AttributesImpl;
061
062/**
063 * Manages the models of skin
064 */
065public class SkinModelsManager extends AbstractLogEnabled implements ThreadSafe, Serviceable, Component, Contextualizable
066{
067    /** The avalon role name */
068    public static final String ROLE = SkinModelsManager.class.getName();
069
070    /** The set of templates classified by skins */
071    protected Map<String, SkinModel> _models = new HashMap<>();
072    
073    /** The avalon service manager */
074    protected ServiceManager _manager;
075    /** The excalibur source resolver */
076    protected SourceResolver _sourceResolver;
077    /** The sites manager */
078    protected SkinsManager _skinsManager;
079    /** Avalon context */
080    protected Context _context;
081    /** Cocoon context */
082    protected org.apache.cocoon.environment.Context _cocoonContext;
083    
084    @Override
085    public void service(ServiceManager manager) throws ServiceException
086    {
087        _manager = manager;
088        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
089        _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE);
090    }
091    
092    @Override
093    public void contextualize(Context context) throws ContextException
094    {
095        _context = context;
096        _cocoonContext = (org.apache.cocoon.environment.Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
097    }
098    
099    /**
100     * Get the list of existing models
101     * @return A set of model names. Can be null if there is an error.
102     * @throws ProcessingException if an error occurs
103     */
104    public Set<String> getModels() throws ProcessingException
105    {
106        try
107        {
108            Set<String> models = new HashSet<>();
109
110            File modelsDir = new File(getModelsLocation());
111            if (modelsDir.exists())
112            {
113                for (File child : modelsDir.listFiles())
114                {
115                    if (_modelExists(child))
116                    {
117                        models.add(child.getName());
118                    }
119                }
120            }
121            
122            return models;
123        }
124        catch (Exception e)
125        {
126            throw new ProcessingException("Can not determine the list of available models", e);
127        }
128    }
129    
130    /**
131     * Get a model
132     * @param id The id of the model
133     * @return The model or null if the model does not exists
134     */
135    @SuppressWarnings("null")
136    public SkinModel getModel(String id)
137    {
138        try
139        {
140            File modelDir = new File (_getModelLocation(id));
141            
142            boolean modelDirExists = _modelExists(modelDir);
143            SkinModel model = _models.get(modelDir.getAbsolutePath());
144            if (model == null && modelDirExists)
145            {
146                model = new SkinModel(id, new File (_getModelLocation(id)));
147                _models.put(modelDir.getAbsolutePath(), model);
148            }
149            else if (!modelDirExists)
150            {
151                _models.put(modelDir.getAbsolutePath(), null);
152                return null;
153            }
154            
155            model.refreshValues();
156            return model;
157        }
158        catch (Exception e)
159        {
160            throw new IllegalStateException("Can get the model of skin for id '" + id + "'", e);
161        }
162    }
163    
164    /**
165     * Get the id of model associated to a skin
166     * @param skin The skin
167     * @return The id of the model or <code>null</code> if there is no model for this skin 
168     */
169    public String getModelOfSkin(Skin skin)
170    {
171        File modelFile = new File (skin.getFile(), "model.xml");
172        if (!modelFile.exists())
173        {
174            // No model
175            return null;
176        }
177        
178        try (InputStream is = new FileInputStream(modelFile))
179        {
180            XPath xpath = XPathFactory.newInstance().newXPath();
181            return xpath.evaluate("model/@id", new InputSource(is));
182        }
183        catch (XPathExpressionException e)
184        {
185            throw new IllegalStateException("The id of model is missing", e);
186        }
187        catch (IOException e)
188        {
189            getLogger().error("Can not determine the model of the skin", e);
190            return null;
191        }
192        
193    }
194    /**
195     * Get hash from model
196     * @param id The id of the model
197     * @return unique has
198     */
199    public String getModelHash (String id)
200    {
201        SkinModel model = getModel(id);
202        String prefix = getModelsLocation();
203        
204        StringBuffer sb = new StringBuffer();
205        List<File> files = (List<File>) FileUtils.listFiles(model.getFile(), FileFileFilter.FILE, TrueFileFilter.INSTANCE);
206        for (File child : files)
207        {
208            sb.append(child.getAbsolutePath().substring(prefix.length() + 1)).append("-").append(child.lastModified()).append(";");
209        }
210        
211        long hash = Math.abs(HashUtil.hash(sb));
212        return Long.toString(hash, 64);
213    }
214    
215    /**
216     * Generates the model.xml file for the skin
217     * @param skinDir The skin directory
218     * @param modelId The model id
219     * @param colorTheme The id of color theme. Can be null.
220     * @throws IOException if an I/O exception occurs during generation
221     * @throws SAXException if an error occurs during generation
222     * @throws TransformerConfigurationException if an error occurs during generation
223     */
224    public void generateModelFile (File skinDir, String modelId, String colorTheme) throws IOException, SAXException, TransformerConfigurationException
225    {
226        File modelFile = new File (skinDir, "model.xml");
227        try (OutputStream os = new FileOutputStream(modelFile))
228        {
229            // create a transformer for saving sax into a file
230            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
231            // create the result where to write
232            StreamResult sResult = new StreamResult(os);
233            th.setResult(sResult);
234
235            // create the format of result
236            Properties format = new Properties();
237            format.put(OutputKeys.METHOD, "xml");
238            format.put(OutputKeys.INDENT, "yes");
239            format.put(OutputKeys.ENCODING, "UTF-8");
240            th.getTransformer().setOutputProperties(format);
241
242            // Send SAX events
243            th.startDocument();
244
245            // Hash for model
246            String hash = getModelHash(modelId);
247            
248            AttributesImpl attrs = new AttributesImpl();
249            attrs.addAttribute("", "id", "id", "CDATA", modelId);
250            attrs.addAttribute("", "hash", "hash", "CDATA", hash);
251            XMLUtils.startElement(th, "model", attrs);
252            
253            XMLUtils.createElement(th, "parameters", "\n");
254            
255            if (colorTheme != null)
256            {
257                XMLUtils.createElement(th, "color-theme", colorTheme);
258            }
259            
260            XMLUtils.endElement(th, "model");
261            
262            th.endDocument();
263        }
264    }
265    
266    /**
267     * Generates the model.xml file for the skin
268     * @param skinDir The skin directory
269     * @param modelId The model id
270     * @throws IOException if an I/O exception occurs during generation
271     * @throws SAXException if an error occurs during generation 
272     * @throws TransformerConfigurationException if an error occurs during generation 
273     */
274    public void generateModelFile (File skinDir, String modelId) throws IOException, SAXException, TransformerConfigurationException
275    {
276        generateModelFile(skinDir, modelId, null);
277    }
278    
279    /**
280     * Get the skins location
281     * @return the skin location
282     */
283    public String getModelsLocation ()
284    {
285        return _cocoonContext.getRealPath("/models");
286    }
287    
288    /**
289     * Get the model location
290     * @param id The id of the model
291     * @return the model location
292     */
293    private String _getModelLocation (String id)
294    {
295        return _cocoonContext.getRealPath("/models/" + id);
296    }
297
298    private boolean _modelExists(File modelDir)
299    {
300        if (!modelDir.exists() || !modelDir.isDirectory())
301        {
302            return false;
303        }
304        
305        File model = new File(modelDir, "model");
306        if (!model.exists() || !model.isDirectory())
307        {
308            return false;
309        }
310        
311        return true;
312    }
313}