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.Map; 024 025import org.apache.avalon.framework.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.logger.AbstractLogEnabled; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.avalon.framework.service.Serviceable; 032import org.apache.cocoon.components.ContextHelper; 033import org.apache.cocoon.environment.Request; 034 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.transformation.ConsistencyChecker.CHECK; 037import org.ametys.core.util.URLEncoder; 038import org.ametys.plugins.repository.AmetysObjectResolver; 039import org.ametys.plugins.repository.metadata.CompositeMetadata; 040import org.ametys.plugins.repository.metadata.File; 041import org.ametys.plugins.repository.metadata.Folder; 042import org.ametys.plugins.repository.metadata.Resource; 043import org.ametys.plugins.repository.metadata.RichText; 044import org.ametys.plugins.repository.metadata.UnknownMetadataException; 045import org.ametys.plugins.repository.version.VersionableAmetysObject; 046import org.ametys.runtime.i18n.I18nizableText; 047import org.ametys.runtime.workspace.WorkspaceMatcher; 048 049/** 050 * {@link URIResolver} for resources local to a Content. 051 */ 052public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable 053{ 054 /** The context */ 055 protected Context _context; 056 /** The ametys object resolver */ 057 protected AmetysObjectResolver _ametysObjectResolver; 058 059 @Override 060 public void service(ServiceManager manager) throws ServiceException 061 { 062 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 063 } 064 065 @Override 066 public void contextualize(Context context) throws ContextException 067 { 068 _context = context; 069 } 070 071 @Override 072 public String getType() 073 { 074 return "local"; 075 } 076 077 @Override 078 public String resolve(String uri, boolean download, boolean absolute, boolean internal) 079 { 080 // uri are like content://UUID@metadata;data/file.ext 081 int i = uri.indexOf('@'); 082 int j = uri.indexOf(';', i); 083 String id = uri.substring(0, i); 084 String metadata = uri.substring(i + 1, j); 085 String path = uri.substring(j + 1); 086 087 try 088 { 089 Request request = ContextHelper.getRequest(_context); 090 091 String contentVersion = null; 092 Content content = (Content) request.getAttribute(Content.class.getName()); 093 if (content != null && id.equals(content.getId())) 094 { 095 contentVersion = ((VersionableAmetysObject) content).getRevision(); 096 } 097 098 StringBuilder resultPath = new StringBuilder(); 099 100 resultPath.append(getUriPrefix(download, absolute, internal)) 101 .append("/plugins/cms" + (download ? "/download/" : "/view/")) 102 .append(path); 103 104 Map<String, String> params = new HashMap<>(); 105 params.put("contentId", id); 106 params.put("metadata", metadata); 107 if (contentVersion != null) 108 { 109 params.put("contentVersion", contentVersion); 110 } 111 112 // Encode twice 113 String encodedPath = URLEncoder.encodePath(resultPath.toString()); 114 return URLEncoder.encodeURI(encodedPath, params); 115 } 116 catch (Exception e) 117 { 118 throw new IllegalStateException(e); 119 } 120 } 121 122 /** 123 * Get the URI prefix 124 * @param download true if the pointed resource is to be downloaded. 125 * @param absolute true if the url must be absolute 126 * @param internal true to get an internal URI. 127 * @return the URI prefix 128 */ 129 protected String getUriPrefix (boolean download, boolean absolute, boolean internal) 130 { 131 if (internal) 132 { 133 return "cocoon://"; 134 } 135 else 136 { 137 Request request = ContextHelper.getRequest(_context); 138 String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI); 139 String uriPrefix = request.getContextPath() + workspaceURI; 140 141 if (absolute && !uriPrefix.startsWith(request.getScheme())) 142 { 143 uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix; 144 } 145 146 return uriPrefix; 147 } 148 } 149 150 @Override 151 public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal) 152 { 153 return resolve(uri, download, absolute, internal); 154 } 155 156 @Override 157 public String resolveImageAsBase64(String uri, int height, int width) 158 { 159 return resolveImageAsBase64(uri, height, width, 0, 0); 160 } 161 162 @Override 163 public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal) 164 { 165 return resolve(uri, download, absolute, internal); 166 } 167 168 @Override 169 public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth) 170 { 171 return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth); 172 } 173 174 /** 175 * Get an image's bytes encoded as base64, optionally resized. 176 * @param uri the image URI. 177 * @param height the specified height. Ignored if negative. 178 * @param width the specified width. Ignored if negative. 179 * @param maxHeight the maximum image height. Ignored if height or width is specified. 180 * @param maxWidth the maximum image width. Ignored if height or width is specified. 181 * @return the image bytes encoded as base64. 182 */ 183 protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth) 184 { 185 // uri are like content://UUID@metadata;data/file.ext 186 int i = uri.indexOf('@'); 187 int j = uri.indexOf(';', i); 188 String id = uri.substring(0, i); 189 String metadata = uri.substring(i + 1, j); 190 String path = uri.substring(j + 1); 191 192 try 193 { 194 Request request = ContextHelper.getRequest(_context); 195 196 Content content = (Content) request.getAttribute(Content.class.getName()); 197 if (!content.getId().equals(id)) 198 { 199 content = _ametysObjectResolver.resolveById(id); 200 } 201 202 RichText richText = _getMeta(content.getMetadataHolder(), metadata); 203 File file = _getFile(richText.getAdditionalDataFolder(), path); 204 Resource resource = file.getResource(); 205 206 try (InputStream dataIs = resource.getInputStream()) 207 { 208 return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth); 209 } 210 } 211 catch (Exception e) 212 { 213 throw new IllegalStateException(e); 214 } 215 } 216 217 @Override 218 public CHECK checkLink(String uri, boolean shortTest) 219 { 220 try 221 { 222 int i = uri.indexOf('@'); 223 int j = uri.indexOf(';', i); 224 225 if (i == -1 || j == -1) 226 { 227 getLogger().warn("Failed to check local URI: '" + uri + " does not respect the excepted format 'content://UUID@metadata;data/file.ext'"); 228 return CHECK.SERVER_ERROR; 229 } 230 231 String id = uri.substring(0, i); 232 String metadata = uri.substring(i + 1, j); 233 String fileName = uri.substring(j + 1); 234 235 Content content = _ametysObjectResolver.resolveById(id); 236 RichText richText = _getMeta(content.getMetadataHolder(), metadata); 237 238 richText.getAdditionalDataFolder().getFile(fileName); 239 240 return CHECK.SUCCESS; 241 } 242 catch (UnknownMetadataException e) 243 { 244 return CHECK.NOT_FOUND; 245 } 246 catch (Exception e) 247 { 248 throw new RuntimeException("Cannot check the uri '" + uri + "'", e); 249 } 250 } 251 252 @Override 253 public I18nizableText getLabel(String uri) 254 { 255 int i = uri.indexOf('@'); 256 int j = uri.indexOf(';', i); 257 258 String fileName = uri.substring(j + 1); 259 260 return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(fileName)); 261 } 262 263 /** 264 * Get the rich text meta 265 * @param meta The composite meta 266 * @param metadataName The metadata name (with /) 267 * @return The rich text meta 268 */ 269 protected RichText _getMeta(CompositeMetadata meta, String metadataName) 270 { 271 int pos = metadataName.indexOf("/"); 272 if (pos == -1) 273 { 274 return meta.getRichText(metadataName); 275 } 276 else 277 { 278 return _getMeta(meta.getCompositeMetadata(metadataName.substring(0, pos)), metadataName.substring(pos + 1)); 279 } 280 } 281 282 /** 283 * Get the file at the specified path in the given folder. 284 * @param folder The folder to search in. 285 * @param path The file path in the folder (can contain slashes). 286 * @return The file if found, null otherwise. 287 */ 288 protected File _getFile(Folder folder, String path) 289 { 290 File file = null; 291 292 Iterator<String> it = Arrays.asList(path.split("/")).iterator(); 293 294 Folder browsedFolder = folder; 295 while (it.hasNext()) 296 { 297 String pathElement = it.next(); 298 299 if (it.hasNext()) 300 { 301 // not the last segment : it is a composite 302 browsedFolder = browsedFolder.getFolder(pathElement); 303 } 304 else 305 { 306 file = browsedFolder.getFile(pathElement); 307 } 308 } 309 310 return file; 311 } 312}