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