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