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}