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}