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 144 try 145 { 146 Request request = ContextHelper.getRequest(_context); 147 148 MetaInfo metaInfo = _getMetaInfo(uri, request); 149 150 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 151 String metadataPath = metaInfo.getMetadataPath(); 152 153 if (object == null) 154 { 155 throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI()); 156 } 157 158 String objectVersion = ""; 159 if (object instanceof VersionableAmetysObject) 160 { 161 objectVersion = ((VersionableAmetysObject) object).getRevision(); 162 } 163 164 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 165 166 StringBuilder resultPath = new StringBuilder(); 167 168 resultPath.append("/_contents-images") 169 .append(object.getPath().replaceAll(":", "%3A")) 170 .append("/_metadata/") 171 .append(metadataPath) 172 .append("_").append(height).append("x").append(width) 173 .append("/").append(URLEncoder.encodePath(binary.getFilename())); 174 175 String path = getUri(resultPath.toString(), object, download, absolute, internal); 176 177 Map<String, String> params = new HashMap<>(); 178 params.put("objectId", object.getId()); 179 if (download) 180 { 181 params.put("download", "true"); 182 } 183 if (objectVersion != null) 184 { 185 params.put("contentVersion", objectVersion); 186 } 187 188 return URLEncoder.encodeURI(path, params); 189 } 190 catch (Exception e) 191 { 192 throw new IllegalStateException(e); 193 } 194 } 195 196 @Override 197 public String resolveImageAsBase64(String uri, int height, int width) 198 { 199 return resolveImageAsBase64(uri, height, width, 0, 0); 200 } 201 202 @Override 203 public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal) 204 { 205 if (maxHeight == 0 && maxWidth == 0) 206 { 207 return resolve(uri, download, absolute, internal); 208 } 209 210 try 211 { 212 Request request = ContextHelper.getRequest(_context); 213 214 MetaInfo metaInfo = _getMetaInfo(uri, request); 215 216 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 217 String metadataPath = metaInfo.getMetadataPath(); 218 219 if (object == null) 220 { 221 throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI()); 222 } 223 224 String objectVersion = ""; 225 if (object instanceof VersionableAmetysObject) 226 { 227 objectVersion = ((VersionableAmetysObject) object).getRevision(); 228 } 229 230 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 231 232 StringBuilder resultPath = new StringBuilder(); 233 234 resultPath.append("/_contents-images") 235 .append(object.getPath().replaceAll(":", "%3A")) 236 .append("/_metadata/") 237 .append(metadataPath) 238 .append("_max").append(maxHeight).append("x").append(maxWidth) 239 .append("/").append(URLEncoder.encodePath(binary.getFilename())); 240 241 String path = getUri(resultPath.toString(), object, download, absolute, internal); 242 243 Map<String, String> params = new HashMap<>(); 244 params.put("objectId", object.getId()); 245 if (download) 246 { 247 params.put("download", "true"); 248 } 249 if (objectVersion != null) 250 { 251 params.put("contentVersion", objectVersion); 252 } 253 254 return URLEncoder.encodeURI(path, params); 255 } 256 catch (Exception e) 257 { 258 throw new IllegalStateException(e); 259 } 260 } 261 262 @Override 263 public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth) 264 { 265 return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth); 266 } 267 268 /** 269 * Get an image's bytes encoded as base64, optionally resized. 270 * @param uri the image URI. 271 * @param height the specified height. Ignored if negative. 272 * @param width the specified width. Ignored if negative. 273 * @param maxHeight the maximum image height. Ignored if height or width is specified. 274 * @param maxWidth the maximum image width. Ignored if height or width is specified. 275 * @return the image bytes encoded as base64. 276 */ 277 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth) 278 { 279 try 280 { 281 Request request = ContextHelper.getRequest(_context); 282 283 MetaInfo metaInfo = _getMetaInfo(uri, request); 284 285 MetadataAwareAmetysObject object = metaInfo.getAmetysObject(); 286 String metadataPath = metaInfo.getMetadataPath(); 287 288 if (object == null) 289 { 290 throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI()); 291 } 292 293 BinaryMetadata binary = getBinaryMetadata(object, metadataPath); 294 295 try (InputStream dataIs = binary.getInputStream()) 296 { 297 return ImageResolverHelper.resolveImageAsBase64(dataIs, binary.getMimeType(), height, width, maxHeight, maxWidth); 298 } 299 } 300 catch (Exception e) 301 { 302 throw new IllegalStateException(e); 303 } 304 } 305 306 /** 307 * Get the URI prefix 308 * @param path the resource path 309 * @param object The object 310 * @param download true if the pointed resource is to be downloaded. 311 * @param absolute true if the url must be absolute 312 * @param internal true to get an internal URI. 313 * @return the URI prefix 314 */ 315 protected String getUri(String path, MetadataAwareAmetysObject object, boolean download, boolean absolute, boolean internal) 316 { 317 if (internal) 318 { 319 return "cocoon://" + path; 320 } 321 else 322 { 323 Request request = ContextHelper.getRequest(_context); 324 String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI); 325 String uriPrefix = request.getContextPath() + workspaceURI; 326 327 if (absolute && !uriPrefix.startsWith(request.getScheme())) 328 { 329 uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix; 330 } 331 332 return uriPrefix + path; 333 } 334 } 335 336 @Override 337 public CHECK checkLink(String uri, boolean shortTest) 338 { 339 return CHECK.SUCCESS; 340 } 341 342 @Override 343 public I18nizableText getLabel(String uri) 344 { 345 Request request = ContextHelper.getRequest(_context); 346 347 MetaInfo metaInfo = _getMetaInfo(uri, request); 348 349 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_LABEL", Collections.singletonList(metaInfo.getMetadataPath())); 350 } 351 352 /** 353 * Get the binary metadata 354 * @param content the content 355 * @param path the metadata path 356 * @return the binary metadata 357 */ 358 protected BinaryMetadata getBinaryMetadata (MetadataAwareAmetysObject content, String path) 359 { 360 CompositeMetadata metadata = content.getMetadataHolder(); 361 362 List<String> pathElements = Arrays.asList(path.split("/")); 363 364 Iterator<String> it = pathElements.iterator(); 365 366 while (it.hasNext()) 367 { 368 String pathElement = it.next(); 369 370 if (it.hasNext()) 371 { 372 // not the last segment : it is a composite 373 metadata = metadata.getCompositeMetadata(pathElement); 374 } 375 else 376 { 377 if (metadata.getType(pathElement) != MetadataType.BINARY) 378 { 379 throw new UnsupportedOperationException("Only binary metadata are allowed"); 380 } 381 382 return metadata.getBinaryMetadata(pathElement); 383 } 384 } 385 386 throw new UnknownMetadataException("Unknown metadata " + path + " for content " + content.getName()); 387 } 388 389 390 /** 391 * Get metadata name and content. 392 * @param uri the metadata URI. 393 * @param request the request. 394 * @return the metadata info. 395 */ 396 protected MetaInfo _getMetaInfo(String uri, Request request) 397 { 398 MetaInfo info = new MetaInfo(); 399 400 Matcher matcher = _OBJECT_URI_PATTERN.matcher(uri); 401 Matcher contentMatcher = _CONTENT_URI_PATTERN.matcher(uri); 402 403 // Test if the URI contains an object ID. 404 if (matcher.matches()) 405 { 406 info.setMetadataPath(matcher.group(1)); 407 String objectId = matcher.group(2); 408 409 MetadataAwareAmetysObject object = _resolver.resolveById(objectId); 410 info.setAmetysObject(object); 411 } 412 else if (contentMatcher.matches()) 413 { 414 // Legacy: handle content ID. 415 info.setMetadataPath(contentMatcher.group(1)); 416 String objectId = contentMatcher.group(2); 417 418 MetadataAwareAmetysObject object = _resolver.resolveById(objectId); 419 info.setAmetysObject(object); 420 } 421 else 422 { 423 // URI without object ID, take the content in the request attributes. 424 info.setMetadataPath(uri); 425 info.setAmetysObject((Content) request.getAttribute(Content.class.getName())); 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 content. 468 * @param object the object to set 469 */ 470 public void setAmetysObject(MetadataAwareAmetysObject object) 471 { 472 this._object = object; 473 } 474 } 475}