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}