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