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        
144        try
145        {
146            Request request = ContextHelper.getRequest(_context);
147            
148            MetaInfo metaInfo = _getMetaInfo(uri, request);
149            
150            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
151            String metadataPath = metaInfo.getMetadataPath();
152            
153            if (object == null)
154            {
155                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
156            }
157            
158            String objectVersion = "";
159            if (object instanceof VersionableAmetysObject)
160            {
161                objectVersion = ((VersionableAmetysObject) object).getRevision();
162            }
163            
164            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
165            
166            StringBuilder resultPath = new StringBuilder();
167            
168            resultPath.append("/_contents-images")
169                .append(object.getPath().replaceAll(":", "%3A"))
170                .append("/_metadata/")
171                .append(metadataPath)
172                .append("_").append(height).append("x").append(width)
173                .append("/").append(URLEncoder.encodePath(binary.getFilename()));
174  
175            String path = getUri(resultPath.toString(), object, download, absolute, internal);
176
177            Map<String, String> params = new HashMap<>();
178            params.put("objectId", object.getId());
179            if (download)
180            {
181                params.put("download", "true");
182            }
183            if (objectVersion != null)
184            {
185                params.put("contentVersion", objectVersion);
186            }
187            
188            return URLEncoder.encodeURI(path, params);
189        }
190        catch (Exception e)
191        {
192            throw new IllegalStateException(e);
193        }
194    }
195    
196    @Override
197    public String resolveImageAsBase64(String uri, int height, int width)
198    {
199        return resolveImageAsBase64(uri, height, width, 0, 0);
200    }
201    
202    @Override
203    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
204    {
205        if (maxHeight == 0 && maxWidth == 0)
206        {
207            return resolve(uri, download, absolute, internal);
208        }
209        
210        try
211        {
212            Request request = ContextHelper.getRequest(_context);
213            
214            MetaInfo metaInfo = _getMetaInfo(uri, request);
215            
216            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
217            String metadataPath = metaInfo.getMetadataPath();
218            
219            if (object == null)
220            {
221                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
222            }
223            
224            String objectVersion = "";
225            if (object instanceof VersionableAmetysObject)
226            {
227                objectVersion = ((VersionableAmetysObject) object).getRevision();
228            }
229            
230            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
231            
232            StringBuilder resultPath = new StringBuilder();
233            
234            resultPath.append("/_contents-images")
235                .append(object.getPath().replaceAll(":", "%3A"))
236                .append("/_metadata/")
237                .append(metadataPath)
238                .append("_max").append(maxHeight).append("x").append(maxWidth)
239                .append("/").append(URLEncoder.encodePath(binary.getFilename()));
240  
241            String path = getUri(resultPath.toString(), object, download, absolute, internal);
242
243            Map<String, String> params = new HashMap<>();
244            params.put("objectId", object.getId());
245            if (download)
246            {
247                params.put("download", "true");
248            }
249            if (objectVersion != null)
250            {
251                params.put("contentVersion", objectVersion);
252            }
253            
254            return URLEncoder.encodeURI(path, params);
255        }
256        catch (Exception e)
257        {
258            throw new IllegalStateException(e);
259        }
260    }
261    
262    @Override
263    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
264    {
265        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth);
266    }
267    
268    /**
269     * Get an image's bytes encoded as base64, optionally resized. 
270     * @param uri the image URI.
271     * @param height the specified height. Ignored if negative.
272     * @param width the specified width. Ignored if negative.
273     * @param maxHeight the maximum image height. Ignored if height or width is specified.
274     * @param maxWidth the maximum image width. Ignored if height or width is specified.
275     * @return the image bytes encoded as base64.
276     */
277    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth)
278    {
279        try
280        {
281            Request request = ContextHelper.getRequest(_context);
282            
283            MetaInfo metaInfo = _getMetaInfo(uri, request);
284            
285            MetadataAwareAmetysObject object = metaInfo.getAmetysObject();
286            String metadataPath = metaInfo.getMetadataPath();
287            
288            if (object == null)
289            {
290                throw new IllegalStateException("Cannot resolve a local link to an unknown content for uri " + request.getRequestURI());
291            }
292            
293            BinaryMetadata binary = getBinaryMetadata(object, metadataPath);
294            
295            try (InputStream dataIs = binary.getInputStream())
296            {
297                return ImageResolverHelper.resolveImageAsBase64(dataIs, binary.getMimeType(), height, width, maxHeight, maxWidth);
298            }
299        }
300        catch (Exception e)
301        {
302            throw new IllegalStateException(e);
303        }
304    }
305    
306    /**
307     * Get the URI prefix
308     * @param path the resource path
309     * @param object The object
310     * @param download true if the pointed resource is to be downloaded.
311     * @param absolute true if the url must be absolute
312     * @param internal true to get an internal URI.
313     * @return the URI prefix
314     */
315    protected String getUri(String path, MetadataAwareAmetysObject object, boolean download, boolean absolute, boolean internal)
316    {
317        if (internal)
318        {
319            return "cocoon://" + path;
320        }
321        else 
322        {
323            Request request = ContextHelper.getRequest(_context);
324            String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
325            String uriPrefix = request.getContextPath() + workspaceURI;
326            
327            if (absolute && !uriPrefix.startsWith(request.getScheme()))
328            {
329                uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
330            }
331            
332            return uriPrefix + path;
333        }
334    }
335    
336    @Override
337    public CHECK checkLink(String uri, boolean shortTest)
338    {
339        return CHECK.SUCCESS;
340    }
341    
342    @Override
343    public I18nizableText getLabel(String uri)
344    {
345        Request request = ContextHelper.getRequest(_context);
346        
347        MetaInfo metaInfo = _getMetaInfo(uri, request);
348        
349        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_METADATA_LABEL", Collections.singletonList(metaInfo.getMetadataPath()));
350    }
351    
352    /**
353     * Get the binary metadata
354     * @param content the content
355     * @param path the metadata path
356     * @return the binary metadata
357     */
358    protected BinaryMetadata getBinaryMetadata (MetadataAwareAmetysObject content, String path)
359    {
360        CompositeMetadata metadata = content.getMetadataHolder();
361        
362        List<String> pathElements = Arrays.asList(path.split("/"));
363        
364        Iterator<String> it = pathElements.iterator();
365        
366        while (it.hasNext())
367        {
368            String pathElement = it.next();
369            
370            if (it.hasNext())
371            {
372                // not the last segment : it is a composite
373                metadata = metadata.getCompositeMetadata(pathElement);
374            }
375            else
376            {
377                if (metadata.getType(pathElement) != MetadataType.BINARY)
378                {
379                    throw new UnsupportedOperationException("Only binary metadata are allowed");
380                }
381                
382                return metadata.getBinaryMetadata(pathElement);
383            }
384        }
385        
386        throw new UnknownMetadataException("Unknown metadata " + path + " for content " + content.getName());
387    }
388    
389    
390    /**
391     * Get metadata name and content.
392     * @param uri the metadata URI.
393     * @param request the request.
394     * @return the metadata info.
395     */
396    protected MetaInfo _getMetaInfo(String uri, Request request)
397    {
398        MetaInfo info = new MetaInfo();
399        
400        Matcher matcher = _OBJECT_URI_PATTERN.matcher(uri);
401        Matcher contentMatcher = _CONTENT_URI_PATTERN.matcher(uri);
402        
403        // Test if the URI contains an object ID.
404        if (matcher.matches())
405        {
406            info.setMetadataPath(matcher.group(1));
407            String objectId = matcher.group(2);
408            
409            MetadataAwareAmetysObject object = _resolver.resolveById(objectId);
410            info.setAmetysObject(object);
411        }
412        else if (contentMatcher.matches())
413        {
414            // Legacy: handle content ID.
415            info.setMetadataPath(contentMatcher.group(1));
416            String objectId = contentMatcher.group(2);
417            
418            MetadataAwareAmetysObject object = _resolver.resolveById(objectId);
419            info.setAmetysObject(object);
420        }
421        else
422        {
423            // URI without object ID, take the content in the request attributes.
424            info.setMetadataPath(uri);
425            info.setAmetysObject((Content) request.getAttribute(Content.class.getName()));
426        }
427        
428        return info;
429    }
430
431    /**
432     * Metadata information.
433     */
434    protected class MetaInfo
435    {
436        private String _metadataPath;
437        private MetadataAwareAmetysObject _object;
438        
439        /**
440         * Get the metadataName.
441         * @return the metadataName
442         */
443        public String getMetadataPath()
444        {
445            return _metadataPath;
446        }
447        
448        /**
449         * Set the metadataPath.
450         * @param metadataPath the metadata path to set
451         */
452        public void setMetadataPath(String metadataPath)
453        {
454            this._metadataPath = metadataPath;
455        }
456        
457        /**
458         * Get the object.
459         * @return the object
460         */
461        public MetadataAwareAmetysObject getAmetysObject()
462        {
463            return _object;
464        }
465        
466        /**
467         * Set the content.
468         * @param object the object to set
469         */
470        public void setAmetysObject(MetadataAwareAmetysObject object)
471        {
472            this._object = object;
473        }
474    }
475}