001/*
002 *  Copyright 2010 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.source;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.net.MalformedURLException;
021import java.util.LinkedHashMap;
022import java.util.Map;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034import org.apache.excalibur.source.Source;
035import org.apache.excalibur.source.SourceFactory;
036import org.apache.excalibur.source.SourceNotFoundException;
037import org.apache.excalibur.source.SourceResolver;
038import org.apache.excalibur.source.SourceValidity;
039
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041import org.ametys.web.repository.page.ZoneItem;
042import org.ametys.web.service.Service;
043import org.ametys.web.service.ServiceExtensionPoint;
044import org.ametys.web.service.ServiceParameter;
045
046/**
047 * This factory looks for files in the current skin and fallback in the current plugin dir.<br>
048 * Use: service://path_to_file<br>
049 * Will first loon in the current template in the <i>stylesheets/services/{pluginName}</i> sub-directory => skins/{skin}/templates/{template}/stylesheets/services/{pluginName}/path_to_file<br>
050 * If not found, then it will look in the skin of the current site in the sub-directory services/{pluginName} => skins/{skin}/services/{currentPluginName}/path_to_file<br>
051 * And if the file does not exist will search in plugin:{currentPluginName}://path_to_file
052 */
053public class ServiceSourceFactory extends AbstractLogEnabled implements SourceFactory, Serviceable, Contextualizable
054{
055    private static final Pattern __SOURCE_PATTERN = Pattern.compile("^[\\w]+:([^:]+:)?//(.*)$");
056
057    private SourceResolver _resolver;
058    private Context _context;
059    private ServiceExtensionPoint _sep;
060    private ServiceManager _manager;
061    
062    /**
063     * The enum of existing types of servicesources 
064     */
065    public enum SourceType 
066    {
067        /** The source is in the plugin */
068        PLUGIN,
069        /** The source is in the skin */
070        SKIN,
071        /** The source is in the template */
072        TEMPLATE
073    }
074    
075    @Override
076    public void contextualize(Context context) throws ContextException
077    {
078        _context = context;
079    }
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        _manager = manager;
085    }
086
087    @Override
088    public Source getSource(String location, Map parameters) throws IOException, MalformedURLException
089    {
090        // lazy initialization to prevent chicken/egg scenario on startup
091        if (_sep == null)
092        {
093            try
094            {
095                _resolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
096                _sep = (ServiceExtensionPoint) _manager.lookup(ServiceExtensionPoint.ROLE);
097            }
098            catch (ServiceException e)
099            {
100                throw new IllegalStateException("Exception while getting components", e);
101            }
102        }
103
104        Matcher m = __SOURCE_PATTERN.matcher(location);
105        if (!m.matches())
106        {
107            throw new MalformedURLException("URI must be like protocol://path/to/resource. Location was '" + location + "'");
108        }
109        
110        Request request = ContextHelper.getRequest(_context);
111        String pluginName = m.group(1);
112        if (pluginName == null)
113        {
114            pluginName = (String) request.getAttribute("pluginName");
115        }
116        else
117        {
118            pluginName = pluginName.substring(0, pluginName.length() - 1);
119        }
120        
121        String uri = m.group(2);
122        
123        if (uri.startsWith("@"))
124        {
125            ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
126            
127            String serviceId = zoneItem.getServiceId();
128            Service service = _sep.getExtension(serviceId);
129            
130            String parameterName = uri.substring(1);
131            String file = null;
132            if (zoneItem.getServiceParameters().hasMetadata(parameterName))
133            {
134                file = zoneItem.getServiceParameters().getString(parameterName);
135            }
136            
137            if (file != null && file.length() > 0)
138            {
139                Source resolverSource = _resolver.resolveURI("service://" + file);
140                if (resolverSource.exists())
141                {
142                    return resolverSource;
143                }
144            }
145
146            getLogger().warn("ZoneItem '{}' references file '{}' that does not exist. Switching to default value.", zoneItem.getId(), file);
147
148            // Fallback to default value
149            ServiceParameter parameter = _findParameter(service, parameterName, location);
150            file = (String) parameter.getDefaultValue();
151            
152            return _resolver.resolveURI("service://" + file);
153        }
154        else
155        {
156            Map<SourceType, String> loc = getLocations(pluginName, uri); 
157            for (SourceType sourceType : loc.keySet())
158            {
159                String sourceUri = loc.get(sourceType); 
160                try
161                {
162                    Source source = _resolver.resolveURI(sourceUri);
163                    if (!source.exists())
164                    {
165                        getLogger().debug("Failed to find a stylesheet at '{}'.", sourceUri);
166                    }
167                    else
168                    {
169                        getLogger().debug("Using source located at '{}'.", sourceUri);
170                        return new ServiceSource(source, sourceType);
171                    }
172                }
173                catch (IOException e)
174                {
175                    getLogger().debug("Resolving protocol failed for resolving '{}'.", sourceUri);
176                }
177            }
178            
179            // Should never occur because of the default stylesheet
180            throw new IOException("Can't find a stylesheet for: " + location);
181        }
182    }
183    
184    private ServiceParameter _findParameter(Service service, String parameterName, String location) throws MalformedURLException
185    {
186        Object parameter = service.getParameters().get(parameterName);
187        
188        if (parameter != null && parameter instanceof ServiceParameter)
189        {
190            return (ServiceParameter) parameter;
191        }
192        
193        throw new MalformedURLException("The service '" + service.getId() + "' does not have a parameter named '" + parameterName + "' that is necessary to resolve '" + location + "'");
194    }
195    
196    /**
197     * Returns the ordered list of URIs to be tested to find the service stylesheet.
198     * @param pluginName the service plugin name.
199     * @param uri the service stylesheet.
200     * @return a list of possible URIs (id is the source type associated).
201     */
202    protected Map<SourceType, String> getLocations(String pluginName, String uri)
203    {
204        Map<SourceType, String> locations = new LinkedHashMap<>();
205        
206        // First look in the current template.
207        locations.put(SourceType.TEMPLATE, "template://stylesheets/services/" + pluginName + "/" + uri);
208        
209        // Then look in the skin.
210        locations.put(SourceType.SKIN, "skin://services/" + pluginName + "/" + uri);
211        
212        // Then look in the plugin.
213        locations.put(SourceType.PLUGIN, "plugin:" + pluginName + "://" + uri);
214        
215        return locations;
216    }
217    
218    @Override
219    public void release(Source source)
220    {
221        // empty method
222    }
223
224    /**
225     * A wrapping source to know real location
226     */
227    public class ServiceSource implements Source
228    {
229        private Source _source;
230        private SourceType _type;
231
232        /**
233         * Creates the service source
234         * @param source The source to wrap
235         * @param type The type of the source
236         */
237        public ServiceSource (Source source, SourceType type)
238        {
239            _type = type;
240            _source = source;
241        }
242        
243        /**
244         * Get the source type of this source
245         * @return the source type
246         */
247        public SourceType getSourceType()
248        {
249            return _type;
250        }
251        
252        /**
253         * Get the wrapped source
254         * @return the original source
255         */
256        public Source getWrappedSource()
257        {
258            return _source;
259        }
260        
261        @Override
262        public boolean exists()
263        {
264            return _source.exists();
265        }
266        
267        @Override
268        public long getContentLength()
269        {
270            return _source.getContentLength();
271        }
272        
273        @Override
274        public InputStream getInputStream() throws IOException, SourceNotFoundException
275        {
276            return _source.getInputStream();
277        }
278        
279        @Override
280        public long getLastModified()
281        {
282            return _source.getLastModified();
283        }
284        
285        @Override
286        public String getMimeType()
287        {
288            return _source.getMimeType();
289        }
290        
291        @Override
292        public String getScheme()
293        {
294            return _source.getScheme();
295        }
296        
297        @Override
298        public String getURI()
299        {
300            return _source.getURI();
301        }
302        
303        @Override
304        public SourceValidity getValidity()
305        {
306            return _source.getValidity();
307        }
308        
309        @Override
310        public void refresh()
311        {
312            _source.refresh();
313        }
314    }
315}