001/* 002 * Copyright 2017 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.plugins.linkdirectory; 017 018import java.io.InputStream; 019import java.util.Arrays; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.apache.avalon.framework.context.Context; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.context.Contextualizable; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036 037import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 038import org.ametys.cms.transformation.ImageResolverHelper; 039import org.ametys.cms.transformation.URIResolver; 040import org.ametys.core.util.FilenameUtils; 041import org.ametys.core.util.URIUtils; 042import org.ametys.plugins.repository.AmetysObjectResolver; 043import org.ametys.plugins.repository.metadata.BinaryMetadata; 044import org.ametys.plugins.repository.metadata.CompositeMetadata; 045import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType; 046import org.ametys.plugins.repository.metadata.MetadataAwareAmetysObject; 047import org.ametys.plugins.repository.metadata.UnknownMetadataException; 048import org.ametys.runtime.i18n.I18nizableText; 049import org.ametys.runtime.plugin.component.PluginAware; 050import org.ametys.web.URIPrefixHandler; 051import org.ametys.web.renderingcontext.RenderingContext; 052import org.ametys.web.renderingcontext.RenderingContextHandler; 053import org.ametys.web.repository.SiteAwareAmetysObject; 054import org.ametys.web.repository.site.Site; 055import org.ametys.web.repository.site.SiteManager; 056 057/** 058 * {@link URIResolver} for type "link-metadata".<br> 059 * These links or images point to a file from the metadata of a {@link Link}. 060 */ 061public class LinkMetadataURIResolver implements URIResolver, Serviceable, Contextualizable, PluginAware 062{ 063 private static final Pattern _OBJECT_URI_PATTERN = Pattern.compile("([^?]*)\\?objectId=(.*)"); 064 065 private AmetysObjectResolver _resolver; 066 private URIPrefixHandler _prefixHandler; 067 private RenderingContextHandler _renderingContexthandler; 068 private SiteManager _siteManager; 069 070 /** The context */ 071 private Context _context; 072 073 private String _pluginName; 074 075 public void setPluginInfo(String pluginName, String featureName, String id) 076 { 077 _pluginName = pluginName; 078 } 079 080 @Override 081 public void contextualize(Context context) throws ContextException 082 { 083 _context = context; 084 } 085 086 @Override 087 public void service(ServiceManager manager) throws ServiceException 088 { 089 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 090 _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE); 091 _renderingContexthandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE); 092 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 093 } 094 095 @Override 096 public String getType() 097 { 098 return "link-metadata"; 099 } 100 101 @Override 102 public String resolve(String uri, boolean download, boolean absolute, boolean internal) 103 { 104 try 105 { 106 Request request = ContextHelper.getRequest(_context); 107 108 MetaInfo metaInfo = _getMetaInfo(uri, request); 109 110 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 111 String metadataPath = metaInfo.getMetadataPath(); 112 113 if (object == null) 114 { 115 throw new IllegalStateException("Cannot resolve a local link to an unknown link for uri " + request.getRequestURI()); 116 } 117 118 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 119 120 StringBuilder resultPath = new StringBuilder(); 121 122 resultPath.append("/_plugins/") 123 .append(_pluginName) 124 .append("/_links") 125 .append(FilenameUtils.encodePath(object.getPath())) 126 .append("/_metadata/") 127 .append(metadataPath) 128 .append("/").append(FilenameUtils.encodeName(binary.getFilename())); 129 130 String path = getUri(resultPath.toString(), object, download, absolute, internal); 131 132 Map<String, String> params = new HashMap<>(); 133 params.put("objectId", object.getId()); 134 if (download) 135 { 136 params.put("download", "true"); 137 } 138 139 return URIUtils.encodeURI(path, params); 140 } 141 catch (Exception e) 142 { 143 throw new IllegalStateException(e); 144 } 145 } 146 147 @Override 148 public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal) 149 { 150 if (height == 0 && width == 0) 151 { 152 return resolve(uri, download, absolute, internal); 153 } 154 StringBuilder uriArgument = new StringBuilder(); 155 uriArgument.append("_").append(height).append("x").append(width); 156 return _resolveImage(uri, uriArgument.toString(), download, absolute, internal); 157 } 158 159 @Override 160 public String resolveImageAsBase64(String uri, int height, int width) 161 { 162 return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0); 163 } 164 165 @Override 166 public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal) 167 { 168 if (maxHeight == 0 && maxWidth == 0) 169 { 170 return resolve(uri, download, absolute, internal); 171 } 172 StringBuilder uriArgument = new StringBuilder(); 173 uriArgument.append("_max").append(maxHeight).append("x").append(maxWidth); 174 return _resolveImage(uri, uriArgument.toString(), download, absolute, internal); 175 } 176 177 @Override 178 public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth) 179 { 180 return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0); 181 } 182 183 @Override 184 public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal) 185 { 186 if (cropHeight == 0 && cropWidth == 0) 187 { 188 return resolve(uri, download, absolute, internal); 189 } 190 StringBuilder uriArgument = new StringBuilder(); 191 uriArgument.append("_crop").append(cropHeight).append("x").append(cropWidth); 192 return _resolveImage(uri, uriArgument.toString(), download, absolute, internal); 193 } 194 195 /** 196 * Resolves a link URI for rendering image.<br> 197 * The output must be a properly encoded path, relative to the webapp context, accessible from a browser. 198 * @param uri the link URI. 199 * @param uriArgument the argument to append to the uri 200 * @param download true if the pointed resource is to be downloaded. 201 * @param absolute true if the url must be absolute 202 * @param internal true to get an internal URI. 203 * @return the path to the image. 204 */ 205 protected String _resolveImage(String uri, String uriArgument, boolean download, boolean absolute, boolean internal) 206 { 207 try 208 { 209 Request request = ContextHelper.getRequest(_context); 210 211 MetaInfo metaInfo = _getMetaInfo(uri, request); 212 213 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 214 String metadataPath = metaInfo.getMetadataPath(); 215 216 if (object == null) 217 { 218 throw new IllegalStateException("Cannot resolve a local link to an unknown link for uri " + request.getRequestURI()); 219 } 220 221 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 222 223 StringBuilder resultPath = new StringBuilder(); 224 225 resultPath.append("/_plugins/") 226 .append(_pluginName) 227 .append("/_links-images") 228 .append(FilenameUtils.encodePath(object.getPath())) 229 .append("/_metadata/") 230 .append(metadataPath) 231 .append(uriArgument) 232 .append("/").append(FilenameUtils.encodeName(binary.getFilename())); 233 234 String path = getUri(resultPath.toString(), object, download, absolute, internal); 235 236 Map<String, String> params = new HashMap<>(); 237 params.put("objectId", object.getId()); 238 if (download) 239 { 240 params.put("download", "true"); 241 } 242 243 return URIUtils.encodeURI(path, params); 244 } 245 catch (Exception e) 246 { 247 throw new IllegalStateException(e); 248 } 249 } 250 251 @Override 252 public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth) 253 { 254 return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth); 255 } 256 257 258 /** 259 * Get an image's bytes encoded as base64, optionally resized. 260 * @param uri the image URI. 261 * @param height the specified height. Ignored if negative. 262 * @param width the specified width. Ignored if negative. 263 * @param maxHeight the maximum image height. Ignored if height or width is specified. 264 * @param maxWidth the maximum image width. Ignored if height or width is specified. 265 * @param cropHeight The cropping height. Ignored if negative. 266 * @param cropWidth The cropping width. Ignored if negative. 267 * @return the image bytes encoded as base64. 268 */ 269 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) 270 { 271 try 272 { 273 Request request = ContextHelper.getRequest(_context); 274 275 MetaInfo metaInfo = _getMetaInfo(uri, request); 276 277 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 278 String metadataPath = metaInfo.getMetadataPath(); 279 280 if (object == null) 281 { 282 throw new IllegalStateException("Cannot resolve a local link to an unknown link for uri " + request.getRequestURI()); 283 } 284 285 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 286 287 try (InputStream dataIs = binary.getInputStream()) 288 { 289 return ImageResolverHelper.resolveImageAsBase64(dataIs, binary.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth); 290 } 291 } 292 catch (Exception e) 293 { 294 throw new IllegalStateException(e); 295 } 296 } 297 298 /** 299 * Get the URI prefix 300 * @param path the resource path 301 * @param object The object 302 * @param download true if the pointed resource is to be downloaded. 303 * @param absolute true if the url must be absolute 304 * @param internal true to get an internal URI. 305 * @return the URI prefix 306 */ 307 protected String getUri(String path, MetadataAwareAmetysObject object, boolean download, boolean absolute, boolean internal) 308 { 309 Request request = ContextHelper.getRequest(_context); 310 311 String siteName = null; 312 if (object instanceof SiteAwareAmetysObject) 313 { 314 siteName = ((SiteAwareAmetysObject) object).getSiteName(); 315 } 316 else 317 { 318 siteName = (String) request.getAttribute("siteName"); 319 320 if (siteName == null) 321 { 322 siteName = (String) request.getAttribute("site"); 323 } 324 } 325 326 if (internal) 327 { 328 return "cocoon://" + siteName + path; 329 } 330 else if (absolute) 331 { 332 if (_renderingContexthandler.getRenderingContext() == RenderingContext.FRONT) 333 { 334 Site site = _siteManager.getSite(siteName); 335 336 String[] aliases = site.getUrlAliases(); 337 return aliases[Math.abs(path.hashCode()) % aliases.length] + path; 338 } 339 340 return _prefixHandler.getAbsoluteUriPrefix() + "/" + siteName + path; 341 } 342 else 343 { 344 return _prefixHandler.getUriPrefix(siteName) + path; 345 } 346 } 347 348 @Override 349 public CHECK checkLink(String uri, boolean shortTest) 350 { 351 return CHECK.SUCCESS; 352 } 353 354 @Override 355 public I18nizableText getLabel(String uri) 356 { 357 Request request = ContextHelper.getRequest(_context); 358 359 MetaInfo metaInfo = _getMetaInfo(uri, request); 360 361 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_LABEL", Collections.singletonList(metaInfo.getMetadataPath())); 362 } 363 364 /** 365 * Get the binary metadata 366 * @param link the link 367 * @param path the metadata path 368 * @return the binary metadata 369 */ 370 protected BinaryMetadata getBinaryMetadata (MetadataAwareAmetysObject link, String path) 371 { 372 CompositeMetadata metadata = link.getMetadataHolder(); 373 374 List<String> pathElements = Arrays.asList(path.split("/")); 375 376 Iterator<String> it = pathElements.iterator(); 377 378 while (it.hasNext()) 379 { 380 String pathElement = it.next(); 381 382 if (it.hasNext()) 383 { 384 // not the last segment : it is a composite 385 metadata = metadata.getCompositeMetadata(pathElement); 386 } 387 else 388 { 389 if (metadata.getType(pathElement) != MetadataType.BINARY) 390 { 391 throw new UnsupportedOperationException("Only binary metadata are allowed"); 392 } 393 394 return metadata.getBinaryMetadata(pathElement); 395 } 396 } 397 398 throw new UnknownMetadataException("Unknown metadata " + path + " for link " + link.getName()); 399 } 400 401 402 /** 403 * Get metadata name and link. 404 * @param uri the metadata URI. 405 * @param request the request. 406 * @return the metadata info. 407 */ 408 protected MetaInfo _getMetaInfo(String uri, Request request) 409 { 410 MetaInfo info = new MetaInfo(); 411 412 Matcher matcher = _OBJECT_URI_PATTERN.matcher(uri); 413 414 // Test if the URI contains an object ID. 415 if (matcher.matches()) 416 { 417 info.setMetadataPath(matcher.group(1)); 418 String objectId = matcher.group(2); 419 420 MetadataAwareAmetysObject object = _resolver.resolveById(objectId); 421 info.setAmetysObject(object); 422 } 423 else 424 { 425 throw new IllegalStateException("Missing objectId parameter to resolve a local link for uri " + request.getRequestURI()); 426 } 427 428 return info; 429 } 430 431 /** 432 * Metadata information. 433 */ 434 protected class MetaInfo 435 { 436 private String _metadataPath; 437 private MetadataAwareAmetysObject _object; 438 439 /** 440 * Get the metadataName. 441 * @return the metadataName 442 */ 443 public String getMetadataPath() 444 { 445 return _metadataPath; 446 } 447 448 /** 449 * Set the metadataPath. 450 * @param metadataPath the metadata path to set 451 */ 452 public void setMetadataPath(String metadataPath) 453 { 454 this._metadataPath = metadataPath; 455 } 456 457 /** 458 * Get the object. 459 * @return the object 460 */ 461 public MetadataAwareAmetysObject getAmetysObject() 462 { 463 return _object; 464 } 465 466 /** 467 * Set the link. 468 * @param object the object to set 469 */ 470 public void setAmetysObject(MetadataAwareAmetysObject object) 471 { 472 this._object = object; 473 } 474 } 475} 476