001/* 002 * Copyright 2020 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.ByteArrayInputStream; 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.nio.charset.StandardCharsets; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.util.ArrayList; 026import java.util.Collections; 027import java.util.List; 028import java.util.Properties; 029import java.util.stream.Collectors; 030 031import javax.xml.transform.OutputKeys; 032import javax.xml.transform.Templates; 033import javax.xml.transform.TransformerConfigurationException; 034import javax.xml.transform.TransformerFactory; 035import javax.xml.transform.TransformerFactoryConfigurationError; 036import javax.xml.transform.sax.SAXTransformerFactory; 037import javax.xml.transform.sax.TransformerHandler; 038import javax.xml.transform.stream.StreamResult; 039import javax.xml.transform.stream.StreamSource; 040 041import org.apache.avalon.framework.component.Component; 042import org.apache.avalon.framework.configuration.Configuration; 043import org.apache.avalon.framework.configuration.ConfigurationException; 044import org.apache.avalon.framework.configuration.DefaultConfiguration; 045import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 046import org.apache.avalon.framework.service.ServiceException; 047import org.apache.avalon.framework.service.ServiceManager; 048import org.apache.avalon.framework.service.Serviceable; 049import org.apache.cocoon.xml.XMLUtils; 050import org.apache.excalibur.xml.sax.SAXParser; 051import org.apache.xml.serializer.OutputPropertiesFactory; 052import org.xml.sax.InputSource; 053import org.xml.sax.SAXException; 054 055import org.ametys.core.util.IgnoreRootHandler; 056 057/** 058 * Helper to read a skin configuration file that will handle inheritance 059 */ 060public class SkinConfigurationHelper implements Component, Serviceable 061{ 062 /** The avalon role */ 063 public static final String ROLE = SkinConfigurationHelper.class.getName(); 064 065 private SkinsManager _skinsManager; 066 067 /** Avalon service manager */ 068 private ServiceManager _manager; 069 070 public void service(ServiceManager manager) throws ServiceException 071 { 072 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 073 _manager = manager; 074 } 075 076 /** 077 * Get the configuration representing a file in a skin taking in account the inheritance process 078 * @param skin The skin to consider 079 * @param relativeConfigurationFileUri The configuration file uri relative to the skin. For example : "conf/tags.xml" 080 * @param relativeDefaultConfigurationFileUri A configuration file uri relative to the skin with default value. 081 * @param mergeXSLIS The XSL file used to merge two configuration file 082 * @return The configuration or null if the configuration file does not exists 083 * @throws IOException If an error occurred while opening the configuration file 084 * @throws ConfigurationException If the configuration file contains errors 085 * @throws SAXException If a XML parsing error occurred in the configuration file 086 */ 087 public Configuration getInheritanceMergedConfiguration(Skin skin, String relativeConfigurationFileUri, String relativeDefaultConfigurationFileUri, InputStream mergeXSLIS) throws IOException, ConfigurationException, SAXException 088 { 089 Templates mergeXSL; 090 try 091 { 092 mergeXSL = TransformerFactory.newInstance().newTemplates(new StreamSource(mergeXSLIS)); 093 } 094 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 095 { 096 throw new IOException(e); 097 } 098 099 String confFileContent = _getInheritanceMergedFile(skin, relativeConfigurationFileUri, mergeXSL); 100 String defaulConfFileContent = _getInheritanceMergedFile(skin, relativeDefaultConfigurationFileUri, mergeXSL); 101 102 String mergedFileContent; 103 104 if (confFileContent == null && defaulConfFileContent == null) 105 { 106 return new DefaultConfiguration("root"); 107 } 108 else if (confFileContent == null) 109 { 110 mergedFileContent = defaulConfFileContent; 111 } 112 else if (defaulConfFileContent == null) 113 { 114 mergedFileContent = confFileContent; 115 } 116 else 117 { 118 mergedFileContent = _merge(confFileContent, defaulConfFileContent, mergeXSL); 119 } 120 121 122 DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(); 123 try (InputStream is = new ByteArrayInputStream(mergedFileContent.getBytes(StandardCharsets.UTF_8))) 124 { 125 return confBuilder.build(is, "skin:" + skin.getId() + "://" + relativeConfigurationFileUri); 126 } 127 } 128 129 /** 130 * Get the configuration representing a file in a skin taking in account the inheritance process 131 * @param skin The skin to consider 132 * @param relativeConfigurationFileUri The configuration file uri relative to the skin. For example : "conf/tags.xml" 133 * @param mergeXSLIS The XSL file used to merge two configuration file 134 * @return The configuration or null if the configuration file does not exists 135 * @throws IOException If an error occurred while opening the configuration file 136 * @throws ConfigurationException If the configuration file contains errors 137 * @throws SAXException If a XML parsing error occurred in the configuration file 138 */ 139 public Configuration getInheritanceMergedConfiguration(Skin skin, String relativeConfigurationFileUri, InputStream mergeXSLIS) throws IOException, ConfigurationException, SAXException 140 { 141 return getInheritanceMergedConfiguration(skin, relativeConfigurationFileUri, null, mergeXSLIS); 142 } 143 144 private String _getInheritanceMergedFile(Skin skin, String relativeConfigurationFileUri, Templates mergeXSL) throws IOException, SAXException 145 { 146 if (relativeConfigurationFileUri == null) 147 { 148 return null; 149 } 150 151 String file = null; 152 153 List<String> parentSkinsIds = new ArrayList<>(skin.getParents()); 154 Collections.reverse(parentSkinsIds); 155 List<Skin> parentSkins = parentSkinsIds.stream() 156 .map(_skinsManager::getSkin) 157 .collect(Collectors.toList()); 158 159 for (Skin parentSkin : parentSkins) 160 { 161 if (parentSkin == null) 162 { 163 throw new IOException("Skin '" + skin.getId() + "' extends an unexisting skin."); 164 } 165 166 file = _merge(_getInheritanceMergedFile(parentSkin, relativeConfigurationFileUri, mergeXSL), file, mergeXSL); 167 } 168 169 return _merge(_getLocalOnlyFileContent(skin, relativeConfigurationFileUri), file, mergeXSL); 170 } 171 172 private String _merge(String strongFile, String weakFile, Templates mergeXSL) throws IOException, SAXException 173 { 174 if (weakFile == null) 175 { 176 return strongFile; 177 } 178 else if (strongFile == null) 179 { 180 return weakFile; 181 } 182 else 183 { 184 try (ByteArrayOutputStream os = new ByteArrayOutputStream(); 185 ByteArrayInputStream strongIs = new ByteArrayInputStream(strongFile.getBytes(StandardCharsets.UTF_8)); 186 ByteArrayInputStream weakIs = new ByteArrayInputStream(weakFile.getBytes(StandardCharsets.UTF_8))) 187 { 188 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(mergeXSL); 189 190 // create the result where to write 191 StreamResult result = new StreamResult(os); 192 th.setResult(result); 193 194 // create the format of result 195 Properties format = new Properties(); 196 format.put(OutputKeys.METHOD, "xml"); 197 format.put(OutputKeys.INDENT, "yes"); 198 format.put(OutputKeys.ENCODING, "UTF-8"); 199 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 200 th.getTransformer().setOutputProperties(format); 201 202 th.startDocument(); 203 XMLUtils.startElement(th, "files"); 204 205 206 SAXParser saxParser = null; 207 try 208 { 209 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 210 211 XMLUtils.startElement(th, "strong"); 212 saxParser.parse(new InputSource(strongIs), new IgnoreRootHandler(th)); 213 XMLUtils.endElement(th, "strong"); 214 215 XMLUtils.startElement(th, "weak"); 216 saxParser.parse(new InputSource(weakIs), new IgnoreRootHandler(th)); 217 XMLUtils.endElement(th, "weak"); 218 } 219 catch (ServiceException e) 220 { 221 throw new RuntimeException("Unable to get a SAX parser", e); 222 } 223 finally 224 { 225 _manager.release(saxParser); 226 } 227 228 XMLUtils.endElement(th, "files"); 229 th.endDocument(); 230 231 return os.toString(StandardCharsets.UTF_8); 232 } 233 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 234 { 235 throw new IOException(e); 236 } 237 } 238 } 239 240 private String _getLocalOnlyFileContent(Skin skin, String relativeConfigurationFileUri) throws IOException 241 { 242 // Here we do not use the skin protocol because we do not want inheritance 243 Path configurationPath = skin.getRawPath().resolve(relativeConfigurationFileUri); 244 if (!Files.exists(configurationPath)) 245 { 246 return null; 247 } 248 249 return Files.readString(configurationPath); 250 } 251}