001/*
002 *  Copyright 2019 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.Collections;
020import java.util.HashMap;
021import java.util.Map;
022
023import javax.jcr.RepositoryException;
024import javax.jcr.Session;
025
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.logger.AbstractLogEnabled;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035
036import org.ametys.cms.URIPrefixHandler;
037import org.ametys.cms.data.Resource;
038import org.ametys.cms.data.RichText;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.transformation.ConsistencyChecker.CHECK;
041import org.ametys.core.util.FilenameUtils;
042import org.ametys.core.util.URIUtils;
043import org.ametys.plugins.repository.AmetysObjectResolver;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.data.UnknownDataException;
046import org.ametys.plugins.repository.version.VersionableAmetysObject;
047import org.ametys.runtime.i18n.I18nizableText;
048
049/**
050 * {@link URIResolver} for resources local to a rich text.
051 */
052public class LocalURIResolver extends AbstractLogEnabled implements URIResolver, Contextualizable, Serviceable
053{
054    /** The context */
055    protected Context _context;
056    /** The ametys object resolver */
057    protected AmetysObjectResolver _ametysObjectResolver;
058    
059    private URIPrefixHandler _prefixHandler;
060
061    @Override
062    public void service(ServiceManager manager) throws ServiceException
063    {
064        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
065        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
066    }
067    
068    @Override
069    public void contextualize(Context context) throws ContextException
070    {
071        _context = context;
072    }
073    
074    @Override
075    public String getType()
076    {
077        return "local";
078    }
079
080    @Override
081    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
082    {
083        URIInfo infos = getInfos(uri, false, null);
084        
085        StringBuilder resultPath = new StringBuilder();
086        
087        resultPath.append(_prefixHandler.computeUriPrefix(absolute, internal))
088                  .append("/plugins/cms/richText-file/")
089                  .append(FilenameUtils.encodeName(infos.getFilename()));
090        
091        Map<String, String> params = new HashMap<>();
092        params.put("contentId", infos.getContentId());
093        params.put("attribute", infos.getAttribute());
094        
095        if (download)
096        {
097            params.put("download", "true");
098        }
099        
100        if (infos.getContentVersion() != null)
101        {
102            params.put("contentVersion", infos.getContentVersion());
103        }
104        
105        // Encode twice
106        return URIUtils.encodeURI(resultPath.toString(), params);
107    }
108    
109    /**
110     * Parses the uri.
111     * @param uri the incoming uri.
112     * @param resolveContent true if the Content should be actually resolved if not found in the request.
113     * @param session the JCR {@link Session} to use, or null to use the current Session.
114     * @return an object containing all parsed infos.
115     */
116    protected URIInfo getInfos(String uri, boolean resolveContent, Session session)
117    {
118        // uri are like <contentId>@<attribute>;<file.ext>
119        int i = uri.indexOf('@');
120        int j = uri.indexOf(';', i);
121        String id = uri.substring(0, i);
122        String attribute = uri.substring(i + 1, j);
123        String filename = uri.substring(j + 1);
124
125        Content content = null;
126        
127        try
128        {
129            Request request  = ContextHelper.getRequest(_context);
130            content = (Content) request.getAttribute(Content.class.getName());
131        }
132        catch (Exception e)
133        {
134            // there's no request, thus no "current" content 
135        }
136
137        String contentVersion = null;
138         
139        if (content == null || !id.equals(content.getId()))
140        {
141            // Some time (such as frontoffice edition) the image is rendered with no content in attribute
142            // The content should be resolved against the given id, but in that case, getRevision will be always the head on
143            try
144            {
145                content = resolveContent ? session == null ? _ametysObjectResolver.resolveById(id) : _ametysObjectResolver.resolveById(id, session) : null;
146            }
147            catch (RepositoryException e)
148            {
149                throw new AmetysRepositoryException(e);
150            }
151        }
152        else
153        {
154            if (content instanceof VersionableAmetysObject)
155            {
156                contentVersion = ((VersionableAmetysObject) content).getRevision();
157            }
158        }
159        
160        URIInfo infos = new URIInfo();
161        infos.setContentId(id);
162        infos.setContentVersion(contentVersion);
163        infos.setContent(content);
164        infos.setAttribute(attribute);
165        infos.setFilename(filename);
166        
167        return infos;
168    }
169    
170    @Override
171    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
172    {
173        return resolve(uri, download, absolute, internal);
174    }
175    
176    @Override
177    public String resolveImageAsBase64(String uri, int height, int width)
178    {
179        return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0);
180    }
181    
182    @Override
183    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
184    {
185        return resolve(uri, download, absolute, internal);
186    }
187    
188    @Override
189    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
190    {
191        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0);
192    }
193    
194    @Override
195    public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal)
196    {
197        return resolve(uri, download, absolute, internal);
198    }
199    
200    @Override
201    public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth)
202    {
203        return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth);
204    }
205    
206    /**
207     * Get an image's bytes encoded as base64, optionally resized. 
208     * @param uri the image URI.
209     * @param height the specified height. Ignored if negative.
210     * @param width the specified width. Ignored if negative.
211     * @param maxHeight the maximum image height. Ignored if height or width is specified.
212     * @param maxWidth the maximum image width. Ignored if height or width is specified.
213     * @param cropHeight The cropping height. Ignored if negative.
214     * @param cropWidth The cropping width. Ignored if negative.
215     * @return the image bytes encoded as base64.
216     */
217    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth)
218    {
219        URIInfo infos = getInfos(uri, true, null);
220        
221        try
222        {
223            RichText richText = infos.getContent().getValue(infos.getAttribute());
224            Resource resource = richText.getAttachment(infos.getFilename());
225            
226            try (InputStream dataIs = resource.getInputStream())
227            {
228                return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth, cropHeight, cropWidth);
229            }
230        }
231        catch (Exception e)
232        {
233            throw new IllegalStateException(e);
234        }
235    }
236    
237    @Override
238    public CHECK checkLink(String uri, boolean shortTest)
239    {
240        try
241        {
242            URIInfo infos = getInfos(uri, true, null);
243            
244            Content content = infos.getContent();
245            RichText richText = content.getValue(infos.getAttribute());
246            richText.getAttachment(infos.getFilename());
247            
248            return CHECK.SUCCESS;
249        }
250        catch (UnknownDataException e)
251        {
252            return CHECK.NOT_FOUND;
253        }
254        catch (Exception e)
255        {
256            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
257        }
258    }
259    
260    @Override
261    public I18nizableText getLabel(String uri)
262    {
263        URIInfo infos = getInfos(uri, false, null);
264        
265        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(infos.getFilename()));
266    }
267    
268    /**
269     * Helper class containg all infos parsed from URI.
270     */
271    protected static class URIInfo
272    {
273        /** The content id. */
274        private String _contentId;
275        /** The relevant attribute */
276        private String _attribute;
277        /** The resource name */
278        private String _filename;
279        /** The content version, if any. */
280        private String _contentVersion;
281        /** The resolved content, if any. */
282        private Content _content;
283        
284        /**
285         * Returns the content id.
286         * @return the content id.
287         */
288        public String getContentId()
289        {
290            return _contentId;
291        }
292        
293        /**
294         * Set the content id.
295         * @param contentId the content id.
296         */
297        public void setContentId(String contentId)
298        {
299            _contentId = contentId;
300        }
301        
302        /**
303         * Returns the metadata.
304         * @return the metadata.
305         */
306        public String getAttribute()
307        {
308            return _attribute;
309        }
310        
311        /**
312         * Set the metadata.
313         * @param attribute the metadata.
314         */
315        public void setAttribute(String attribute)
316        {
317            _attribute = attribute;
318        }
319        
320        /**
321         * Returns the resource name.
322         * @return the name
323         */
324        public String getFilename()
325        {
326            return _filename;
327        }
328        
329        /**
330         * Set the resource name.
331         * @param name the name.
332         */
333        public void setFilename(String name)
334        {
335            _filename = name;
336        }
337        
338        /**
339         * Returns the content version, if any.
340         * @return the content version.
341         */
342        public String getContentVersion()
343        {
344            return _contentVersion;
345        }
346        
347        /**
348         * Set the content version.
349         * @param contentVersion the content version.
350         */
351        public void setContentVersion(String contentVersion)
352        {
353            _contentVersion = contentVersion;
354        }
355        
356        /**
357         * Returns the resolved content, if any.
358         * @return the content.
359         */
360        public Content getContent()
361        {
362            return _content;
363        }
364        
365        /**
366         * Set the content.
367         * @param content the content.
368         */
369        public void setContent(Content content)
370        {
371            _content = content;
372        }
373    }
374}