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}