001/*
002 *  Copyright 2017 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.glyph;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import javax.xml.parsers.SAXParserFactory;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.configuration.Configuration;
031import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.cocoon.components.ContextHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.commons.lang.StringUtils;
041import org.apache.excalibur.source.Source;
042import org.apache.excalibur.source.SourceResolver;
043import org.xml.sax.XMLReader;
044
045import org.ametys.core.ui.Callable;
046import org.ametys.plugins.core.ui.glyph.CssFontHelper;
047import org.ametys.runtime.plugin.component.AbstractLogEnabled;
048import org.ametys.web.repository.site.Site;
049import org.ametys.web.repository.site.SiteManager;
050
051/**
052 * Manager for skin glyph sources
053 */
054public class SkinGlyphSourceManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
055{
056    /** The Avalon Role */
057    public static final String ROLE = SkinGlyphSourceManager.class.getName();
058
059    private CssFontHelper _cssFontHelper;
060    private SourceResolver _resolver;
061    private SiteManager _siteManager;
062    private Context _context;
063    
064    private long _lastUpdate;
065    private Map<String, List<String>> _cssFiles = new HashMap<>();
066    
067    @Override
068    public void service(ServiceManager smanager) throws ServiceException
069    {
070        _cssFontHelper = (CssFontHelper) smanager.lookup(CssFontHelper.ROLE);
071        _siteManager = (SiteManager) smanager.lookup(SiteManager.ROLE);
072        _resolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
073    }
074    
075    @Override
076    public void contextualize(Context context) throws ContextException
077    {
078        _context = context;
079    }
080
081    /**
082     * Determines if the skin has available glyphesIde
083     * @param siteName The site name
084     * @return true if the skin has available glyphes
085     * @throws Exception if failed to read skin files
086     */
087    @Callable
088    public boolean hasGlyphs(String siteName) throws Exception
089    {
090        Site site = _siteManager.getSite(siteName);
091        String skinName = site.getSkinId();
092        
093        return !getGlyphs(skinName).isEmpty();
094    }
095    
096    /**
097     * Get the glyphs provided by the skin
098     * @param skinName The skin name
099     * @return The CSS class names for glyphs
100     * @throws Exception if failed to read skin files
101     */
102    @Callable
103    public List<Map<String, String>> getGlyphsStore(String skinName) throws Exception
104    {
105        List<Map<String, String>> glyphs = new ArrayList<>();
106
107        Set<String> glyphClassNames = getGlyphs(skinName);
108        for (String glyphClassName : glyphClassNames)
109        {
110            Map<String, String> glyph = new HashMap<>();
111            glyph.put("cssClassName", glyphClassName);
112            glyphs.add(glyph);
113        }
114        
115        return glyphs;
116    }
117    
118    /**
119     * Get the CSS class names for glyphes contained in the skin
120     * @param skinName The skin name
121     * @return The CSS class names for glyphes
122     * @throws Exception if failed to read skin files
123     */
124    public Set<String> getGlyphs(String skinName) throws Exception
125    {
126        Set<String> glyphes = new LinkedHashSet<>();
127
128        List<String> relativePathsWithFontFace = getRelativeCssFilesWithFontFace(skinName);
129        for (String relativePath : relativePathsWithFontFace)
130        {
131            String sourceUri = "skin://resources/" + relativePath;
132            glyphes.addAll(_cssFontHelper.getGlyphClassNames(sourceUri, null));
133        }
134        
135        return glyphes;
136    }
137    
138    /**
139     * Get the CSS files with 'font-face' rule in the skin
140     * @param siteName The site name
141     * @return The CSS files with font
142     * @throws Exception If failed to get the list of CSS files with 'font-face' rule
143     */
144    @Callable
145    public Map<String, Object> getCSSFiles(String siteName) throws Exception
146    {
147        Request request = ContextHelper.getRequest(_context);
148
149        Site site = _siteManager.getSite(siteName);
150        String skinName = site.getSkinId();
151
152        String resourcePath = request.getContextPath() + "/skins/" + skinName + "/resources/";
153
154        Map<String, Object> result = new HashMap<>();
155
156        if (StringUtils.isNotBlank(skinName))
157        {
158            List<String> relativePathsWithFontFace = getRelativeCssFilesWithFontFace(skinName);
159
160            List<String> cssPaths = new ArrayList<>();
161            for (String relativePath : relativePathsWithFontFace)
162            {
163                cssPaths.add(resourcePath + relativePath);
164            }
165            result.put("cssFiles", cssPaths);
166        }
167        return result;
168    }
169
170    /**
171     * Get the URI of CSS files with the 'font-face' rule 
172     * @param skinName The skin name
173     * @return the URI of CSS files with the 'font-face' rule 
174     * @throws Exception if fails to read conf/glyph.xml file
175     */
176    public List<String> getRelativeCssFilesWithFontFace(String skinName) throws Exception
177    {
178        List<String> relativeCssFiles = _getRelativeCssFiles(skinName);
179
180        List<String> relativeCssFilesWithFontFace = new ArrayList<>();
181        for (String relativeCssFile : relativeCssFiles)
182        {
183            String cssURI = "skin://resources/" + relativeCssFile;
184            if (_cssFontHelper.hasFontFaceRule(cssURI))
185            {
186                relativeCssFilesWithFontFace.add(relativeCssFile);
187            }
188            else
189            {
190                getLogger().warn("Font-face rule was not found in CSS style sheet '" + cssURI + "'. It will be ignored");
191            }
192        }
193        
194        return relativeCssFilesWithFontFace;
195    }
196
197    /**
198     * Get the URI of CSS files listed in conf/glyph.xml file
199     * @param skinName The skin name
200     * @return The list of css files
201     * @throws Exception if fails to read conf/glyph.xml file
202     */
203    private List<String> _getRelativeCssFiles(String skinName) throws Exception
204    {
205        Source src = null;
206        try
207        {
208            SAXParserFactory factory = SAXParserFactory.newInstance();
209
210            XMLReader reader = factory.newSAXParser().getXMLReader();
211
212            DefaultConfigurationBuilder confBuilder = new DefaultConfigurationBuilder(reader);
213
214            String confFileUri = "context://skins/" + skinName + "/conf/fonts.xml";
215            
216            src = _resolver.resolveURI(confFileUri);
217
218            if (src.exists())
219            {
220                if (_lastUpdate < src.getLastModified())
221                {
222                    List<String> files = new ArrayList<>();
223                    
224                    try (InputStream is = src.getInputStream())
225                    {
226
227                        Configuration configuration = confBuilder.build(is, src.getURI());
228                        Configuration[] filesConfiguration = configuration.getChildren("file");
229
230                        for (Configuration fileConfiguration : filesConfiguration)
231                        {
232                            String file = fileConfiguration.getValue();
233                            files.add(file);
234                        }
235                    }
236                    
237                    _cssFiles.put(skinName, files);
238                    _lastUpdate = src.getLastModified();
239                }
240                
241                return _cssFiles.get(skinName);
242            }
243        }
244        finally
245        {
246            _resolver.release(src);
247        }
248        
249        return Collections.EMPTY_LIST;
250    }
251}