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 SourceNotFoundException("Can't find a stylesheet for: " + location);
184        }
185    }
186    
187    private ElementDefinition _findParameter(Service service, String parameterName, String location) throws MalformedURLException
188    {
189        if (service.hasModelItem(parameterName))
190        {
191            ModelItem parameter = service.getModelItem(parameterName);
192            
193            if (parameter instanceof ElementDefinition)
194            {
195                return (ElementDefinition) parameter;
196            }
197        }
198        
199        throw new MalformedURLException("The service '" + service.getId() + "' does not have a parameter named '" + parameterName + "' that is necessary to resolve '" + location + "'");
200    }
201    
202    /**
203     * Returns the ordered list of URIs to be tested to find the service stylesheet.
204     * @param pluginName the service plugin name.
205     * @param uri the service stylesheet.
206     * @return a list of possible URIs (id is the source type associated).
207     */
208    protected Map<SourceType, String> getLocations(String pluginName, String uri)
209    {
210        Map<SourceType, String> locations = new LinkedHashMap<>();
211        
212        // First look in the current template.
213        locations.put(SourceType.TEMPLATE, "template://stylesheets/services/" + pluginName + "/" + uri);
214        
215        // Then look in the skin.
216        locations.put(SourceType.SKIN, "skin://services/" + pluginName + "/" + uri);
217        
218        // Then look in the plugin.
219        locations.put(SourceType.PLUGIN, "plugin:" + pluginName + "://" + uri);
220        
221        return locations;
222    }
223    
224    @Override
225    public void release(Source source)
226    {
227        // empty method
228    }
229
230    /**
231     * A wrapping source to know real location
232     */
233    public class ServiceSource implements Source
234    {
235        private Source _source;
236        private SourceType _type;
237
238        /**
239         * Creates the service source
240         * @param source The source to wrap
241         * @param type The type of the source
242         */
243        public ServiceSource (Source source, SourceType type)
244        {
245            _type = type;
246            _source = source;
247        }
248        
249        /**
250         * Get the source type of this source
251         * @return the source type
252         */
253        public SourceType getSourceType()
254        {
255            return _type;
256        }
257        
258        /**
259         * Get the wrapped source
260         * @return the original source
261         */
262        public Source getWrappedSource()
263        {
264            return _source;
265        }
266        
267        @Override
268        public boolean exists()
269        {
270            return _source.exists();
271        }
272        
273        @Override
274        public long getContentLength()
275        {
276            return _source.getContentLength();
277        }
278        
279        @Override
280        public InputStream getInputStream() throws IOException, SourceNotFoundException
281        {
282            return _source.getInputStream();
283        }
284        
285        @Override
286        public long getLastModified()
287        {
288            return _source.getLastModified();
289        }
290        
291        @Override
292        public String getMimeType()
293        {
294            return _source.getMimeType();
295        }
296        
297        @Override
298        public String getScheme()
299        {
300            return _source.getScheme();
301        }
302        
303        @Override
304        public String getURI()
305        {
306            return _source.getURI();
307        }
308        
309        @Override
310        public SourceValidity getValidity()
311        {
312            return _source.getValidity();
313        }
314        
315        @Override
316        public void refresh()
317        {
318            _source.refresh();
319        }
320    }
321}