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.nio.charset.StandardCharsets; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Optional; 026 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.cocoon.components.ContextHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.http.NameValuePair; 037import org.apache.http.client.utils.URLEncodedUtils; 038 039import org.ametys.cms.data.Binary; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 042import org.ametys.core.util.FilenameUtils; 043import org.ametys.core.util.ImageResolverHelper; 044import org.ametys.core.util.URIUtils; 045import org.ametys.plugins.repository.AmetysObject; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.UnknownAmetysObjectException; 048import org.ametys.plugins.repository.data.ametysobject.ModelAwareDataAwareAmetysObject; 049import org.ametys.plugins.repository.data.external.ExternalizableDataProvider.ExternalizableDataStatus; 050import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper; 051import org.ametys.plugins.repository.data.holder.values.ValueContext; 052import org.ametys.plugins.repository.version.VersionAwareAmetysObject; 053import org.ametys.plugins.repository.version.VersionableAmetysObject; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.workspace.WorkspaceMatcher; 056 057/** 058 * {@link URIResolver} for type "attribute".<br> 059 * These links or images point to a file from the attribute of the current ametys object. 060 */ 061public class AttributeURIResolver extends AbstractURIResolver implements Serviceable, Contextualizable 062{ 063 /** Parameter name for the status of value to get for the attribute */ 064 public static final String EXTERNALIZABLE_DATA_STATUS_PARAM_NAME = "externalizableDataStatus"; 065 066 private static final String __OBJECT_ID_URI_QUERY_PARAM_NAME = "objectId"; 067 private static final String __CONTENT_ID_URI_QUERY_PARAM_NAME = "contentId"; 068 069 /** The ametys object resolver */ 070 protected AmetysObjectResolver _resolver; 071 072 /** The context */ 073 protected Context _context; 074 075 @Override 076 public void contextualize(Context context) throws ContextException 077 { 078 _context = context; 079 } 080 081 @Override 082 public void service(ServiceManager manager) throws ServiceException 083 { 084 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 085 } 086 087 @Override 088 public String getType() 089 { 090 return "attribute"; 091 } 092 093 private record AmetysObjectAndRevision (ModelAwareDataAwareAmetysObject ametysObject, boolean hasSwitched, Optional<String> revision) { /* empty */ } 094 095 @Override 096 protected String _resolve(String uri, String uriArgument, boolean download, boolean absolute, boolean internal) 097 { 098 Request request = ContextHelper.getRequest(_context); 099 AttributeInfo info = _getAttributeInfo(uri, request); 100 101 AmetysObjectAndRevision ametysObjectAndRevision = _getAmetysObjectAndRevisionFromAttributeInfo(info); 102 ModelAwareDataAwareAmetysObject ametysObject = ametysObjectAndRevision.ametysObject(); 103 104 try 105 { 106 String ametysObjectVersion = StringUtils.EMPTY; 107 if (ametysObject instanceof VersionableAmetysObject) 108 { 109 ametysObjectVersion = ((VersionableAmetysObject) ametysObject).getRevision(); 110 } 111 112 String path = info.getPath(); 113 Optional<ExternalizableDataStatus> status = info.getStatus(); 114 Binary binary = DataHolderHelper.getValue(ametysObject, path, ValueContext.newInstance().withStatus(status.orElse(null))); 115 116 String filename = FilenameUtils.encodeName(binary.getFilename()); 117 118 String baseName = org.apache.commons.io.FilenameUtils.getBaseName(filename); 119 String extension = org.apache.commons.io.FilenameUtils.getExtension(filename); 120 121 StringBuilder resultPath = new StringBuilder(); 122 123 resultPath.append("/_object") 124 .append(FilenameUtils.encodePath(ametysObject.getPath())) 125 .append("/_attribute/") 126 .append(path) 127 .append("/") 128 .append(baseName) 129 .append(uriArgument) 130 .append(extension.isEmpty() ? "" : "." + extension); 131 132 String resultUri = getUri(resultPath.toString(), ametysObject, download, absolute, internal); 133 134 Map<String, String> params = new HashMap<>(); 135 params.put("objectId", ametysObject.getId()); 136 137 if (status.isPresent()) 138 { 139 params.put(EXTERNALIZABLE_DATA_STATUS_PARAM_NAME, status.get().name()); 140 } 141 142 if (download) 143 { 144 params.put("download", "true"); 145 } 146 147 if (ametysObjectVersion != null) 148 { 149 params.put("contentVersion", ametysObjectVersion); 150 } 151 152 return internal ? URIUtils.buildURI(resultUri, params) : URIUtils.encodeURI(resultUri, params); 153 } 154 finally 155 { 156 _resetAmetysObjectRevisionIfNeeded(ametysObjectAndRevision); 157 } 158 159 } 160 161 @Override 162 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth) 163 { 164 Request request = ContextHelper.getRequest(_context); 165 AttributeInfo info = _getAttributeInfo(uri, request); 166 167 AmetysObjectAndRevision ametysObjectAndRevision = _getAmetysObjectAndRevisionFromAttributeInfo(info); 168 ModelAwareDataAwareAmetysObject ametysObject = ametysObjectAndRevision.ametysObject(); 169 170 try 171 { 172 String path = info.getPath(); 173 Binary binary = ametysObject.getValue(path); 174 175 try (InputStream dataIs = binary.getInputStream()) 176 { 177 return ImageResolverHelper.resolveImageAsBase64(dataIs, binary.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth); 178 } 179 } 180 catch (Exception e) 181 { 182 throw new IllegalStateException(e); 183 } 184 finally 185 { 186 _resetAmetysObjectRevisionIfNeeded(ametysObjectAndRevision); 187 } 188 } 189 190 public String getMimeType(String uri) 191 { 192 Request request = ContextHelper.getRequest(_context); 193 AttributeInfo info = _getAttributeInfo(uri, request); 194 195 AmetysObjectAndRevision ametysObjectAndRevision = _getAmetysObjectAndRevisionFromAttributeInfo(info); 196 ModelAwareDataAwareAmetysObject ametysObject = ametysObjectAndRevision.ametysObject(); 197 198 try 199 { 200 String path = info.getPath(); 201 Binary binary = ametysObject.getValue(path); 202 return binary.getMimeType(); 203 } 204 catch (Exception e) 205 { 206 throw new IllegalStateException(e); 207 } 208 finally 209 { 210 _resetAmetysObjectRevisionIfNeeded(ametysObjectAndRevision); 211 } 212 } 213 214 private AmetysObjectAndRevision _getAmetysObjectAndRevisionFromAttributeInfo(AttributeInfo info) 215 { 216 ModelAwareDataAwareAmetysObject ametysObject = info.getAmetysObject(); 217 218 if (ametysObject == null) 219 { 220 Request request = ContextHelper.getRequest(_context); 221 throw new IllegalStateException("Cannot resolve a local link to an unknown ametys object for uri " + request.getRequestURI()); 222 } 223 224 boolean hasToSwitch = _hasToSwitchRevision(info); 225 Optional<String> currentRevision = Optional.empty(); 226 if (hasToSwitch && ametysObject instanceof VersionAwareAmetysObject versionAwareAO) 227 { 228 // Keep current revision to switch again later 229 currentRevision = Optional.ofNullable(versionAwareAO.getRevision()); 230 231 // Switch to targeted revision 232 versionAwareAO.switchToRevision(info.getAmetysObjectVersion().orElse(null)); 233 } 234 235 return new AmetysObjectAndRevision(ametysObject, hasToSwitch, currentRevision); 236 } 237 238 /** 239 * Checks in {@link AttributeInfo} if the ametys object has the same revision than the targeted one 240 * @param infos the {@link AttributeInfo} containing the ametys object and the targeted revision (=ametysObjectVersion) 241 * @return <code>true</code> if a switch is needed, <code>false</code> otherwise 242 */ 243 private boolean _hasToSwitchRevision(AttributeInfo infos) 244 { 245 ModelAwareDataAwareAmetysObject ametysObject = infos.getAmetysObject(); 246 247 if (ametysObject instanceof VersionAwareAmetysObject versionAwareAO) 248 { 249 String currentRevision = versionAwareAO.getRevision(); 250 Optional<String> targetRevision = infos.getAmetysObjectVersion(); 251 252 return currentRevision == null && targetRevision.isPresent() // ametys object is at latest revision but another one has been targeted 253 || targetRevision.isEmpty() // ametys object is at specific revision but latest has been targeted 254 || !targetRevision.get().equals(currentRevision); // ametys object is at specific revision but another one has been targeted 255 } 256 257 return false; 258 } 259 260 private void _resetAmetysObjectRevisionIfNeeded(AmetysObjectAndRevision ametysObjectAndRevision) 261 { 262 if (ametysObjectAndRevision.hasSwitched() && ametysObjectAndRevision.ametysObject() instanceof VersionAwareAmetysObject versionAwareAO) 263 { 264 // Switch again the content revision 265 Optional<String> oldRevision = ametysObjectAndRevision.revision(); 266 versionAwareAO.switchToRevision(oldRevision.orElse(null)); 267 } 268 } 269 270 /** 271 * Get the URI prefix 272 * @param path the resource path 273 * @param object The object 274 * @param download true if the pointed resource is to be downloaded. 275 * @param absolute true if the url must be absolute 276 * @param internal true to get an internal URI. 277 * @return the URI prefix 278 */ 279 protected String getUri(String path, ModelAwareDataAwareAmetysObject object, boolean download, boolean absolute, boolean internal) 280 { 281 if (internal) 282 { 283 return "cocoon://" + path; 284 } 285 else 286 { 287 Request request = ContextHelper.getRequest(_context); 288 String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI); 289 String uriPrefix = request.getContextPath() + workspaceURI; 290 291 if (absolute && !uriPrefix.startsWith(request.getScheme())) 292 { 293 uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix; 294 } 295 296 return uriPrefix + path; 297 } 298 } 299 300 @Override 301 public CHECK checkLink(String uri, boolean shortTest) 302 { 303 return CHECK.SUCCESS; 304 } 305 306 @Override 307 public I18nizableText getLabel(String uri) 308 { 309 try 310 { 311 Request request = ContextHelper.getRequest(_context); 312 313 AttributeInfo info = _getAttributeInfo(uri, request); 314 315 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_LABEL", Collections.singletonList(info.getPath())); 316 } 317 catch (UnknownAmetysObjectException e) 318 { 319 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_UNKNOWN"); 320 } 321 } 322 323 /** 324 * Get attribute path and ametys object. 325 * @param uri the attribute URI. 326 * @param request the request. 327 * @return the attribute info. 328 */ 329 protected AttributeInfo _getAttributeInfo(String uri, Request request) 330 { 331 AttributeInfo info = new AttributeInfo(); 332 333 int i = uri.indexOf('?'); 334 335 String attributePath = i < 0 ? uri : uri.substring(0, i); 336 info.setPath(attributePath); 337 338 List<NameValuePair> queryParams = Optional.of(i) 339 .filter(index -> index >= 0) 340 .map(index -> uri.substring(index + 1)) 341 .map(queryParamsAsString -> URLEncodedUtils.parse(queryParamsAsString, StandardCharsets.UTF_8)) 342 .orElseGet(ArrayList::new); 343 344 ModelAwareDataAwareAmetysObject ametysObject = (ModelAwareDataAwareAmetysObject) request.getAttribute(Content.class.getName()); 345 346 Optional<String> objectId = _getQueryParamValue(queryParams, __OBJECT_ID_URI_QUERY_PARAM_NAME) 347 .or(() -> _getQueryParamValue(queryParams, __CONTENT_ID_URI_QUERY_PARAM_NAME)); 348 if (objectId.isPresent() && (ametysObject == null || !objectId.get().equals(ametysObject.getId()))) 349 { 350 ametysObject = _resolver.resolveById(objectId.get()); 351 } 352 353 info.setAmetysObject(ametysObject); 354 355 _getQueryParamValue(queryParams, "contentVersion").ifPresent(info::setAmetysObjectVersion); 356 357 ExternalizableDataStatus status = _getQueryParamValue(queryParams, EXTERNALIZABLE_DATA_STATUS_PARAM_NAME) 358 .map(ExternalizableDataStatus::valueOf) 359 .orElse(null); 360 info.setStatus(status); 361 362 return info; 363 } 364 365 private Optional<String> _getQueryParamValue(List<NameValuePair> queryParams, String queryParamName) 366 { 367 return queryParams.stream() 368 .filter(pair -> queryParamName.equals(pair.getName())) 369 .map(NameValuePair::getValue) 370 .findFirst(); 371 } 372 373 /** 374 * Attribute information. 375 */ 376 protected class AttributeInfo 377 { 378 private String _path; 379 private ModelAwareDataAwareAmetysObject _ametysObject; 380 private Optional<String> _ametysObjectVersion = Optional.empty(); 381 private Optional<ExternalizableDataStatus> _status = Optional.empty(); 382 383 /** 384 * Get the attribute path. 385 * @return the attribute path 386 */ 387 public String getPath() 388 { 389 return _path; 390 } 391 392 /** 393 * Set the attribute path. 394 * @param path the attribute path to set 395 */ 396 public void setPath(String path) 397 { 398 _path = path; 399 } 400 401 /** 402 * Get the ametys object. 403 * @return the ametys object 404 */ 405 public ModelAwareDataAwareAmetysObject getAmetysObject() 406 { 407 return _ametysObject; 408 } 409 410 /** 411 * Set the ametys object. 412 * @param ametysObject the {@link AmetysObject} to set 413 */ 414 public void setAmetysObject(ModelAwareDataAwareAmetysObject ametysObject) 415 { 416 _ametysObject = ametysObject; 417 } 418 419 /** 420 * Get the ametys object's version. 421 * @return the ametys object's version 422 */ 423 public Optional<String> getAmetysObjectVersion() 424 { 425 return _ametysObjectVersion; 426 } 427 428 /** 429 * Set the ametys object's version. 430 * @param ametysObjectVersion the version to set 431 */ 432 public void setAmetysObjectVersion(String ametysObjectVersion) 433 { 434 _ametysObjectVersion = Optional.ofNullable(ametysObjectVersion); 435 } 436 437 /** 438 * Get the externalizable data status. 439 * @return the externalizable data status 440 */ 441 public Optional<ExternalizableDataStatus> getStatus() 442 { 443 return _status; 444 } 445 446 /** 447 * Set the externalizable data status. 448 * @param status the externalizable data status to set 449 */ 450 public void setStatus(ExternalizableDataStatus status) 451 { 452 _status = Optional.ofNullable(status); 453 } 454 } 455}