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 mergeXSLIS The XSL file used to merge two configuration file 081 * @return The configuration or null if the configuration file does not exists 082 * @throws IOException If an error occurred while opening the configuration file 083 * @throws ConfigurationException If the configuration file contains errors 084 * @throws SAXException If a XML parsing error occurred in the configuration file 085 */ 086 public Configuration getInheritanceMergedConfiguration(Skin skin, String relativeConfigurationFileUri, InputStream mergeXSLIS) throws IOException, ConfigurationException, SAXException 087 { 088 Templates mergeXSL; 089 try 090 { 091 mergeXSL = TransformerFactory.newInstance().newTemplates(new StreamSource(mergeXSLIS)); 092 } 093 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 094 { 095 throw new IOException(e); 096 } 097 098 String confFileContent = _getInheritanceMergedFile(skin, relativeConfigurationFileUri, mergeXSL); 099 if (confFileContent == null) 100 { 101 return new DefaultConfiguration("root"); 102 } 103 104 DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(); 105 try (InputStream is = new ByteArrayInputStream(confFileContent.getBytes(StandardCharsets.UTF_8))) 106 { 107 return confBuilder.build(is, "skin:" + skin.getId() + "://" + relativeConfigurationFileUri); 108 } 109 } 110 111 private String _getInheritanceMergedFile(Skin skin, String relativeConfigurationFileUri, Templates mergeXSL) throws IOException, SAXException 112 { 113 String file = null; 114 115 List<String> parentSkinsIds = new ArrayList<>(skin.getParents()); 116 Collections.reverse(parentSkinsIds); 117 List<Skin> parentSkins = parentSkinsIds.stream() 118 .map(_skinsManager::getSkin) 119 .collect(Collectors.toList()); 120 121 for (Skin parentSkin : parentSkins) 122 { 123 if (parentSkin == null) 124 { 125 throw new IOException("Skin '" + skin.getId() + "' extends an unexisting skin."); 126 } 127 128 file = _merge(_getInheritanceMergedFile(parentSkin, relativeConfigurationFileUri, mergeXSL), file, mergeXSL); 129 } 130 131 return _merge(_getLocalOnlyFileContent(skin, relativeConfigurationFileUri), file, mergeXSL); 132 } 133 134 private String _merge(String strongFile, String weakFile, Templates mergeXSL) throws IOException, SAXException 135 { 136 if (weakFile == null) 137 { 138 return strongFile; 139 } 140 else if (strongFile == null) 141 { 142 return weakFile; 143 } 144 else 145 { 146 try (ByteArrayOutputStream os = new ByteArrayOutputStream(); 147 ByteArrayInputStream strongIs = new ByteArrayInputStream(strongFile.getBytes(StandardCharsets.UTF_8)); 148 ByteArrayInputStream weakIs = new ByteArrayInputStream(weakFile.getBytes(StandardCharsets.UTF_8))) 149 { 150 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(mergeXSL); 151 152 // create the result where to write 153 StreamResult result = new StreamResult(os); 154 th.setResult(result); 155 156 // create the format of result 157 Properties format = new Properties(); 158 format.put(OutputKeys.METHOD, "xml"); 159 format.put(OutputKeys.INDENT, "yes"); 160 format.put(OutputKeys.ENCODING, "UTF-8"); 161 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 162 th.getTransformer().setOutputProperties(format); 163 164 th.startDocument(); 165 XMLUtils.startElement(th, "files"); 166 167 168 SAXParser saxParser = null; 169 try 170 { 171 saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE); 172 173 XMLUtils.startElement(th, "strong"); 174 saxParser.parse(new InputSource(strongIs), new IgnoreRootHandler(th)); 175 XMLUtils.endElement(th, "strong"); 176 177 XMLUtils.startElement(th, "weak"); 178 saxParser.parse(new InputSource(weakIs), new IgnoreRootHandler(th)); 179 XMLUtils.endElement(th, "weak"); 180 } 181 catch (ServiceException e) 182 { 183 throw new RuntimeException("Unable to get a SAX parser", e); 184 } 185 finally 186 { 187 _manager.release(saxParser); 188 } 189 190 XMLUtils.endElement(th, "files"); 191 th.endDocument(); 192 193 return os.toString(StandardCharsets.UTF_8); 194 } 195 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 196 { 197 throw new IOException(e); 198 } 199 } 200 } 201 202 private String _getLocalOnlyFileContent(Skin skin, String relativeConfigurationFileUri) throws IOException 203 { 204 // Here we do not use the skin protocol because we do not want inheritance 205 Path configurationPath = skin.getRawPath().resolve(relativeConfigurationFileUri); 206 if (!Files.exists(configurationPath)) 207 { 208 return null; 209 } 210 211 return Files.readString(configurationPath); 212 } 213}