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