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}