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.FilenameUtils;
040import org.ametys.core.util.URIUtils;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.metadata.BinaryMetadata;
043import org.ametys.plugins.repository.metadata.CompositeMetadata;
044import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
045import org.ametys.plugins.repository.metadata.MetadataAwareAmetysObject;
046import org.ametys.plugins.repository.metadata.UnknownMetadataException;
047import org.ametys.plugins.repository.version.VersionableAmetysObject;
048import org.ametys.runtime.i18n.I18nizableText;
049import org.ametys.runtime.workspace.WorkspaceMatcher;
050
051/**
052 * {@link URIResolver} for type "metadata".<br>
053 * These links or images point to a file from the metadata of the current Content.
054 */
055public class MetadataURIResolver implements URIResolver, Serviceable, Contextualizable
056{
057    private static final Pattern _OBJECT_URI_PATTERN = Pattern.compile("([^?]*)\\?objectId=(.*)");
058    private static final Pattern _CONTENT_URI_PATTERN = Pattern.compile("([^?]*)\\?contentId=(.*)");
059    
060    /** The ametys object resolver */
061    protected AmetysObjectResolver _resolver;
062    /** The context */
063    protected Context _context;
064    
065    @Override
066    public void contextualize(Context context) throws ContextException
067    {
068        _context = context;
069    }
070    
071    @Override
072    public void service(ServiceManager manager) throws ServiceException
073    {
074        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
075    }
076    
077    @Override
078    public String getType()
079    {
080        return "metadata";
081    }
082    
083    @Override
084    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
085    {
086        try
087        {
088            Request request = ContextHelper.getRequest(_context);
089            
090            MetaInfo metaInfo = _getMetaInfo(uri, request);
091            
092            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
093            String metadataPath = metaInfo.getMetadataPath();
094            
095            if (object == null)
096            {
097                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
098            }
099            
100            String objectVersion = "";
101            if (object instanceof VersionableAmetysObject)
102            {
103                objectVersion = ((VersionableAmetysObject) object).getRevision();
104            }
105            
106            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
107            
108            StringBuilder resultPath = new StringBuilder();
109            
110            resultPath.append("/_contents")
111                    .append(FilenameUtils.encodePath(object.getPath()))
112                    .append("/_metadata/")
113                    .append(metadataPath)
114                    .append("/").append(FilenameUtils.encodeName(binary.getFilename()));
115              
116            String path = getUri(resultPath.toString(), object, download, absolute, internal);
117              
118            Map<String, String> params = new HashMap<>();
119            params.put("objectId", object.getId());
120            if (download)
121            {
122                params.put("download", "true");
123            }
124            if (objectVersion != null)
125            {
126                params.put("contentVersion", objectVersion);
127            }
128            
129            return URIUtils.encodeURI(path, params);
130        }
131        catch (Exception e)
132        {
133            throw new IllegalStateException(e);
134        }
135    }
136    
137    @Override
138    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
139    {
140        if (height == 0 && width == 0)
141        {
142            return resolve(uri, download, absolute, internal);
143        }
144        StringBuilder uriArgument = new StringBuilder();
145        uriArgument.append("_").append(height).append("x").append(width);
146        return _resolveImage(uri, uriArgument.toString(), download, absolute, internal);
147    }
148    
149    @Override
150    public String resolveImageAsBase64(String uri, int height, int width)
151    {
152        return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0);
153    }
154    
155    @Override
156    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
157    {
158        if (maxHeight == 0 && maxWidth == 0)
159        {
160            return resolve(uri, download, absolute, internal);
161        }
162        StringBuilder uriArgument = new StringBuilder();
163        uriArgument.append("_max").append(maxHeight).append("x").append(maxWidth);
164        return _resolveImage(uri, uriArgument.toString(), download, absolute, internal);
165    }
166    
167    
168    @Override
169    public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal)
170    {
171        if (cropHeight == 0 && cropWidth == 0)
172        {
173            return resolve(uri, download, absolute, internal);
174        }
175        StringBuilder uriArgument = new StringBuilder();
176        uriArgument.append("_crop").append(cropHeight).append("x").append(cropWidth);
177        return _resolveImage(uri, uriArgument.toString(), download, absolute, internal);
178    }
179    
180    @Override
181    public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth)
182    {
183        return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth);
184    }
185    
186    /**
187     * Resolves a link URI for rendering image.<br>
188     * The output must be a properly encoded path, relative to the webapp context, accessible from a browser.
189     * @param uri the link URI.
190     * @param uriArgument the argument to append to the uri
191     * @param download true if the pointed resource is to be downloaded.
192     * @param absolute true if the url must be absolute
193     * @param internal true to get an internal URI.
194     * @return the path to the image.
195     */
196    protected String _resolveImage(String uri, String uriArgument, boolean download, boolean absolute, boolean internal)
197    {
198        try
199        {
200            Request request = ContextHelper.getRequest(_context);
201            
202            MetaInfo metaInfo = _getMetaInfo(uri, request);
203            
204            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
205            String metadataPath = metaInfo.getMetadataPath();
206            
207            if (object == null)
208            {
209                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
210            }
211            
212            String objectVersion = "";
213            if (object instanceof VersionableAmetysObject)
214            {
215                objectVersion = ((VersionableAmetysObject) object).getRevision();
216            }
217            
218            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
219            
220            StringBuilder resultPath = new StringBuilder();
221            
222            resultPath.append("/_contents-images")
223                .append(FilenameUtils.encodePath(object.getPath()))
224                .append("/_metadata/")
225                .append(metadataPath)
226                .append(uriArgument)
227                .append("/").append(FilenameUtils.encodeName(binary.getFilename()));
228  
229            String path = getUri(resultPath.toString(), object, download, absolute, internal);
230
231            Map<String, String> params = new HashMap<>();
232            params.put("objectId", object.getId());
233            if (download)
234            {
235                params.put("download", "true");
236            }
237            if (objectVersion != null)
238            {
239                params.put("contentVersion", objectVersion);
240            }
241            
242            return internal ? URIUtils.buildURI(path, params) : URIUtils.encodeURI(path, params);
243        }
244        catch (Exception e)
245        {
246            throw new IllegalStateException(e);
247        }
248    }
249    
250    @Override
251    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
252    {
253        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0);
254    }
255    
256    /**
257     * Get an image's bytes encoded as base64, optionally resized. 
258     * @param uri the image URI.
259     * @param height the specified height. Ignored if negative.
260     * @param width the specified width. Ignored if negative.
261     * @param maxHeight the maximum image height. Ignored if height or width is specified.
262     * @param maxWidth the maximum image width. Ignored if height or width is specified.
263     * @param cropHeight The cropping height. Ignored if negative.
264     * @param cropWidth The cropping width. Ignored if negative.
265     * @return the image bytes encoded as base64.
266     */
267    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth)
268    {
269        try
270        {
271            Request request = ContextHelper.getRequest(_context);
272            
273            MetaInfo metaInfo = _getMetaInfo(uri, request);
274            
275            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
276            String metadataPath = metaInfo.getMetadataPath();
277            
278            if (object == null)
279            {
280                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
281            }
282            
283            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
284            
285            try (InputStream dataIs = binary.getInputStream())
286            {
287                return ImageResolverHelper.resolveImageAsBase64(dataIs, binary.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth);
288            }
289        }
290        catch (Exception e)
291        {
292            throw new IllegalStateException(e);
293        }
294    }
295    
296    /**
297     * Get the URI prefix
298     * @param path the resource path
299     * @param object The object
300     * @param download true if the pointed resource is to be downloaded.
301     * @param absolute true if the url must be absolute
302     * @param internal true to get an internal URI.
303     * @return the URI prefix
304     */
305    protected String getUri(String path, MetadataAwareAmetysObject object, boolean download, boolean absolute, boolean internal)
306    {
307        if (internal)
308        {
309            return "cocoon://" + path;
310        }
311        else 
312        {
313            Request request = ContextHelper.getRequest(_context);
314            String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
315            String uriPrefix = request.getContextPath() + workspaceURI;
316            
317            if (absolute && !uriPrefix.startsWith(request.getScheme()))
318            {
319                uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
320            }
321            
322            return uriPrefix + path;
323        }
324    }
325    
326    @Override
327    public CHECK checkLink(String uri, boolean shortTest)
328    {
329        return CHECK.SUCCESS;
330    }
331    
332    @Override
333    public I18nizableText getLabel(String uri)
334    {
335        Request request = ContextHelper.getRequest(_context);
336        
337        MetaInfo metaInfo = _getMetaInfo(uri, request);
338        
339        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_LABEL", Collections.singletonList(metaInfo.getMetadataPath()));
340    }
341    
342    /**
343     * Get the binary metadata
344     * @param content the content
345     * @param path the metadata path
346     * @return the binary metadata
347     */
348    protected BinaryMetadata getBinaryMetadata (MetadataAwareAmetysObject content, String path)
349    {
350        CompositeMetadata metadata = content.getMetadataHolder();
351        
352        List<String> pathElements = Arrays.asList(path.split("/"));
353        
354        Iterator<String> it = pathElements.iterator();
355        
356        while (it.hasNext())
357        {
358            String pathElement = it.next();
359            
360            if (it.hasNext())
361            {
362                // not the last segment : it is a composite
363                metadata = metadata.getCompositeMetadata(pathElement);
364            }
365            else
366            {
367                if (metadata.getType(pathElement) != MetadataType.BINARY)
368                {
369                    throw new UnsupportedOperationException("Only binary metadata are allowed");
370                }
371                
372                return metadata.getBinaryMetadata(pathElement);
373            }
374        }
375        
376        throw new UnknownMetadataException("Unknown metadata " + path + " for content " + content.getName());
377    }
378    
379    
380    /**
381     * Get metadata name and content.
382     * @param uri the metadata URI.
383     * @param request the request.
384     * @return the metadata info.
385     */
386    protected MetaInfo _getMetaInfo(String uri, Request request)
387    {
388        MetaInfo info = new MetaInfo();
389        
390        Matcher matcher = _OBJECT_URI_PATTERN.matcher(uri);
391        Matcher contentMatcher = _CONTENT_URI_PATTERN.matcher(uri);
392        
393        // Test if the URI contains an object ID.
394        if (matcher.matches())
395        {
396            info.setMetadataPath(matcher.group(1));
397            String objectId = matcher.group(2);
398            
399            MetadataAwareAmetysObject object = _resolver.resolveById(objectId);
400            info.setAmetysObject(object);
401        }
402        else if (contentMatcher.matches())
403        {
404            // Legacy: handle content ID.
405            info.setMetadataPath(contentMatcher.group(1));
406            String objectId = contentMatcher.group(2);
407            
408            MetadataAwareAmetysObject object = _resolver.resolveById(objectId);
409            info.setAmetysObject(object);
410        }
411        else
412        {
413            // URI without object ID, take the content in the request attributes.
414            info.setMetadataPath(uri);
415            info.setAmetysObject((Content) request.getAttribute(Content.class.getName()));
416        }
417        
418        return info;
419    }
420
421    /**
422     * Metadata information.
423     */
424    protected class MetaInfo
425    {
426        private String _metadataPath;
427        private MetadataAwareAmetysObject _object;
428        
429        /**
430         * Get the metadataName.
431         * @return the metadataName
432         */
433        public String getMetadataPath()
434        {
435            return _metadataPath;
436        }
437        
438        /**
439         * Set the metadataPath.
440         * @param metadataPath the metadata path to set
441         */
442        public void setMetadataPath(String metadataPath)
443        {
444            this._metadataPath = metadataPath;
445        }
446        
447        /**
448         * Get the object.
449         * @return the object
450         */
451        public MetadataAwareAmetysObject getAmetysObject()
452        {
453            return _object;
454        }
455        
456        /**
457         * Set the content.
458         * @param object the object to set
459         */
460        public void setAmetysObject(MetadataAwareAmetysObject object)
461        {
462            this._object = object;
463        }
464    }
465}