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