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