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