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}