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