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}