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.DefaultConfigurationBuilder; 045import org.apache.avalon.framework.service.ServiceException; 046import org.apache.avalon.framework.service.ServiceManager; 047import org.apache.avalon.framework.service.Serviceable; 048import org.apache.cocoon.xml.XMLUtils; 049import org.apache.excalibur.xml.sax.SAXParser; 050import org.apache.xml.serializer.OutputPropertiesFactory; 051import org.xml.sax.InputSource; 052import org.xml.sax.SAXException; 053 054import org.ametys.core.util.IgnoreRootHandler; 055 056/** 057 * Helper to read a skin configuration file that will handle inheritance 058 */ 059public class SkinConfigurationHelper implements Component, Serviceable 060{ 061 /** The avalon role */ 062 public static final String ROLE = SkinConfigurationHelper.class.getName(); 063 064 private SkinsManager _skinsManager; 065 private SAXParser _saxParser; 066 067 public void service(ServiceManager manager) throws ServiceException 068 { 069 _skinsManager = (SkinsManager) manager.lookup(SkinsManager.ROLE); 070 _saxParser = (SAXParser) manager.lookup(SAXParser.ROLE); 071 } 072 073 /** 074 * Get the configuration representing a file in a skin taking in account the inheritance process 075 * @param skin The skin to consider 076 * @param relativeConfigurationFileUri The configuration file uri relative to the skin. For example : "conf/tags.xml" 077 * @param mergeXSLIS The XSL file used to merge two configuration file 078 * @return The configuration or null if the configuration file does not exists 079 * @throws IOException If an error occurred while opening the configuration file 080 * @throws ConfigurationException If the configuration file contains errors 081 * @throws SAXException If a XML parsing error occurred in the configuration file 082 */ 083 public Configuration getInheritanceMergedConfiguration(Skin skin, String relativeConfigurationFileUri, InputStream mergeXSLIS) throws IOException, ConfigurationException, SAXException 084 { 085 Templates mergeXSL; 086 try 087 { 088 mergeXSL = TransformerFactory.newInstance().newTemplates(new StreamSource(mergeXSLIS)); 089 } 090 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 091 { 092 throw new IOException(e); 093 } 094 095 String confFileContent = _getInheritanceMergedFile(skin, relativeConfigurationFileUri, mergeXSL); 096 097 DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(); 098 try (InputStream is = new ByteArrayInputStream(confFileContent.getBytes(StandardCharsets.UTF_8))) 099 { 100 return confBuilder.build(is, "skin:" + skin.getId() + "://" + relativeConfigurationFileUri); 101 } 102 } 103 104 private String _getInheritanceMergedFile(Skin skin, String relativeConfigurationFileUri, Templates mergeXSL) throws IOException, SAXException 105 { 106 String file = null; 107 108 List<String> parentSkinsIds = new ArrayList<>(skin.getParents()); 109 Collections.reverse(parentSkinsIds); 110 List<Skin> parentSkins = parentSkinsIds.stream() 111 .map(_skinsManager::getSkin) 112 .collect(Collectors.toList()); 113 114 for (Skin parentSkin : parentSkins) 115 { 116 if (parentSkin == null) 117 { 118 throw new IOException("Skin '" + skin.getId() + "' extends an unexisting skin."); 119 } 120 121 file = _merge(_getInheritanceMergedFile(parentSkin, relativeConfigurationFileUri, mergeXSL), file, mergeXSL); 122 } 123 124 return _merge(_getLocalOnlyFileContent(skin, relativeConfigurationFileUri), file, mergeXSL); 125 } 126 127 private String _merge(String strongFile, String weakFile, Templates mergeXSL) throws IOException, SAXException 128 { 129 if (weakFile == null) 130 { 131 return strongFile; 132 } 133 else if (strongFile == null) 134 { 135 return weakFile; 136 } 137 else 138 { 139 try (ByteArrayOutputStream os = new ByteArrayOutputStream(); 140 ByteArrayInputStream strongIs = new ByteArrayInputStream(strongFile.getBytes(StandardCharsets.UTF_8)); 141 ByteArrayInputStream weakIs = new ByteArrayInputStream(weakFile.getBytes(StandardCharsets.UTF_8))) 142 { 143 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(mergeXSL); 144 145 // create the result where to write 146 StreamResult result = new StreamResult(os); 147 th.setResult(result); 148 149 // create the format of result 150 Properties format = new Properties(); 151 format.put(OutputKeys.METHOD, "xml"); 152 format.put(OutputKeys.INDENT, "yes"); 153 format.put(OutputKeys.ENCODING, "UTF-8"); 154 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 155 th.getTransformer().setOutputProperties(format); 156 157 th.startDocument(); 158 XMLUtils.startElement(th, "files"); 159 160 XMLUtils.startElement(th, "strong"); 161 _saxParser.parse(new InputSource(strongIs), new IgnoreRootHandler(th)); 162 XMLUtils.endElement(th, "strong"); 163 164 XMLUtils.startElement(th, "weak"); 165 _saxParser.parse(new InputSource(weakIs), new IgnoreRootHandler(th)); 166 XMLUtils.endElement(th, "weak"); 167 168 XMLUtils.endElement(th, "files"); 169 th.endDocument(); 170 171 172 return os.toString(StandardCharsets.UTF_8); 173 } 174 catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) 175 { 176 throw new IOException(e); 177 } 178 } 179 } 180 181 private String _getLocalOnlyFileContent(Skin skin, String relativeConfigurationFileUri) throws IOException 182 { 183 // Here we do not use the skin protocol because we do not want inheritance 184 Path configurationPath = skin.getRawPath().resolve(relativeConfigurationFileUri); 185 if (!Files.exists(configurationPath)) 186 { 187 return null; 188 } 189 190 return Files.readString(configurationPath); 191 } 192}