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