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.Arrays;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.Map;
024
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.avalon.framework.logger.AbstractLogEnabled;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034
035import org.ametys.cms.repository.Content;
036import org.ametys.cms.transformation.ConsistencyChecker.CHECK;
037import org.ametys.core.util.URLEncoder;
038import org.ametys.plugins.repository.AmetysObjectResolver;
039import org.ametys.plugins.repository.metadata.CompositeMetadata;
040import org.ametys.plugins.repository.metadata.File;
041import org.ametys.plugins.repository.metadata.Folder;
042import org.ametys.plugins.repository.metadata.Resource;
043import org.ametys.plugins.repository.metadata.RichText;
044import org.ametys.plugins.repository.metadata.UnknownMetadataException;
045import org.ametys.plugins.repository.version.VersionableAmetysObject;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.workspace.WorkspaceMatcher;
048
049/**
050 * {@link URIResolver} for resources local to a Content.
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    @Override
060    public void service(ServiceManager manager) throws ServiceException
061    {
062        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
063    }
064    
065    @Override
066    public void contextualize(Context context) throws ContextException
067    {
068        _context = context;
069    }
070    
071    @Override
072    public String getType()
073    {
074        return "local";
075    }
076
077    @Override
078    public String resolve(String uri, boolean download, boolean absolute, boolean internal)
079    {
080        URIInfo infos = getInfos(uri, false);
081        
082        StringBuilder resultPath = new StringBuilder();
083        
084        resultPath.append(getUriPrefix(download, absolute, internal))
085                  .append("/plugins/cms" + (download ? "/download/" : "/view/"))
086                  .append(infos.getPath());
087        
088        Map<String, String> params = new HashMap<>();
089        params.put("contentId", infos.getContentId());
090        params.put("metadata", infos.getMetadata());
091        if (infos.getContentVersion() != null)
092        {
093            params.put("contentVersion", infos.getContentVersion());
094        }
095        
096        // Encode twice
097        String encodedPath = URLEncoder.encodePath(resultPath.toString());
098        return URLEncoder.encodeURI(encodedPath, params);
099    }
100    
101    /**
102     * Parses the uri.
103     * @param uri the incoming uri.
104     * @param resolveContent true if the Content should be actually resolved if not found in the request.
105     * @return an object containing all parsed infos.
106     */
107    protected URIInfo getInfos(String uri, boolean resolveContent)
108    {
109        // uri are like content://UUID@metadata;data/file.ext
110        int i = uri.indexOf('@');
111        int j = uri.indexOf(';', i);
112        String id = uri.substring(0, i);
113        String metadata = uri.substring(i + 1, j);
114        String path = uri.substring(j + 1);
115
116        Request request = ContextHelper.getRequest(_context);
117        
118        String contentVersion = null;
119        // The content should be the one from request (UUID) but in that case, getRevision will be always the head on
120        Content content = (Content) request.getAttribute(Content.class.getName()); 
121        if (content == null || !id.equals(content.getId()))
122        {
123            // Some time (such as frontoffice edition) the image is rendered with no content in attrbiute
124            content = resolveContent ? _ametysObjectResolver.resolveById(id) : null;
125        }
126        else
127        {
128            contentVersion = ((VersionableAmetysObject) content).getRevision();
129        }
130        
131        URIInfo infos = new URIInfo();
132        infos.setContentId(id);
133        infos.setContentVersion(contentVersion);
134        infos.setMetadata(metadata);
135        infos.setPath(path);
136        infos.setContent(content);
137        
138        return infos;
139    }
140    
141    /**
142     * Get the URI prefix
143     * @param download true if the pointed resource is to be downloaded.
144     * @param absolute true if the url must be absolute
145     * @param internal true to get an internal URI.
146     * @return the URI prefix
147     */
148    protected String getUriPrefix (boolean download, boolean absolute, boolean internal)
149    {
150        if (internal)
151        {
152            return "cocoon://";
153        }
154        else 
155        {
156            Request request = ContextHelper.getRequest(_context);
157            String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
158            String uriPrefix = request.getContextPath() + workspaceURI;
159            
160            if (absolute && !uriPrefix.startsWith(request.getScheme()))
161            {
162                uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
163            }
164            
165            return uriPrefix;
166        }
167    }
168    
169    @Override
170    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
171    {
172        throw new UnsupportedOperationException("resolveImage");
173    }
174    
175    @Override
176    public String resolveImageAsBase64(String uri, int height, int width)
177    {
178        return resolveImageAsBase64(uri, height, width, 0, 0, 0, 0);
179    }
180    
181    @Override
182    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
183    {
184        throw new UnsupportedOperationException("resolveBoundedImage");
185    }
186    
187    @Override
188    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
189    {
190        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth, 0, 0);
191    }
192    
193    @Override
194    public String resolveCroppedImage(String uri, int cropHeight, int cropWidth, boolean download, boolean absolute, boolean internal)
195    {
196        throw new UnsupportedOperationException("resolveCroppedImage");
197    }
198    
199    @Override
200    public String resolveCroppedImageAsBase64(String uri, int cropHeight, int cropWidth)
201    {
202        return resolveImageAsBase64(uri, 0, 0, 0, 0, cropHeight, cropWidth);
203    }
204    
205    /**
206     * Get an image's bytes encoded as base64, optionally resized. 
207     * @param uri the image URI.
208     * @param height the specified height. Ignored if negative.
209     * @param width the specified width. Ignored if negative.
210     * @param maxHeight the maximum image height. Ignored if height or width is specified.
211     * @param maxWidth the maximum image width. Ignored if height or width is specified.
212     * @param cropHeight The cropping height. Ignored if negative.
213     * @param cropWidth The cropping width. Ignored if negative.
214     * @return the image bytes encoded as base64.
215     */
216    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth, int cropHeight, int cropWidth)
217    {
218        URIInfo infos = getInfos(uri, true);
219        
220        try
221        {
222            RichText richText = _getMeta(infos.getContent().getMetadataHolder(), infos.getMetadata());
223            File file = _getFile(richText.getAdditionalDataFolder(), infos.getPath());
224            Resource resource = file.getResource();
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            int i = uri.indexOf('@');
243            int j = uri.indexOf(';', i);
244            
245            if (i == -1 || j == -1)
246            {
247                getLogger().warn("Failed to check local URI: '" + uri + " does not respect the excepted format 'content://UUID@metadata;data/file.ext'");
248                return CHECK.SERVER_ERROR;
249            }
250            
251            String id = uri.substring(0, i);
252            String metadata = uri.substring(i + 1, j);
253            String fileName = uri.substring(j + 1);
254            
255            Content content = _ametysObjectResolver.resolveById(id);
256            RichText richText = _getMeta(content.getMetadataHolder(), metadata);
257            
258            richText.getAdditionalDataFolder().getFile(fileName);
259            
260            return CHECK.SUCCESS;
261        }
262        catch (UnknownMetadataException e)
263        {
264            return CHECK.NOT_FOUND;
265        }
266        catch (Exception e)
267        {
268            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
269        }
270    }
271    
272    @Override
273    public I18nizableText getLabel(String uri)
274    {
275        int i = uri.indexOf('@');
276        int j = uri.indexOf(';', i);
277        
278        String fileName = uri.substring(j + 1);
279        
280        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(fileName));
281    }
282    
283    /**
284     * Get the rich text meta
285     * @param meta The composite meta
286     * @param metadataName The metadata name (with /)
287     * @return The rich text meta
288     */
289    protected RichText _getMeta(CompositeMetadata meta, String metadataName)
290    {
291        int pos = metadataName.indexOf("/");
292        if (pos == -1)
293        {
294            return meta.getRichText(metadataName);
295        }
296        else
297        {
298            return _getMeta(meta.getCompositeMetadata(metadataName.substring(0, pos)), metadataName.substring(pos + 1));
299        }
300    }
301    
302    /**
303     * Get the file at the specified path in the given folder.
304     * @param folder The folder to search in.
305     * @param path The file path in the folder (can contain slashes).
306     * @return The file if found, null otherwise.
307     */
308    protected File _getFile(Folder folder, String path)
309    {
310        File file = null;
311        
312        Iterator<String> it = Arrays.asList(path.split("/")).iterator();
313        
314        Folder browsedFolder = folder;
315        while (it.hasNext())
316        {
317            String pathElement = it.next();
318            
319            if (it.hasNext())
320            {
321                // not the last segment : it is a composite
322                browsedFolder = browsedFolder.getFolder(pathElement);
323            }
324            else
325            {
326                file = browsedFolder.getFile(pathElement);
327            }
328        }
329        
330        return file;
331    }
332    
333    /**
334     * Helper class containg all infos parsed from URI.
335     */
336    protected static class URIInfo
337    {
338        /** The content id. */
339        private String _contentId;
340        /** The relevant attribute */
341        private String _metadata;
342        /** The path to the resource */
343        private String _path;
344        /** The content version, if any. */
345        private String _contentVersion;
346        /** The resolved content, if any. */
347        private Content _content;
348        
349        /**
350         * Returns the content id.
351         * @return the content id.
352         */
353        public String getContentId()
354        {
355            return _contentId;
356        }
357        
358        /**
359         * Set the content id.
360         * @param contentId the content id.
361         */
362        public void setContentId(String contentId)
363        {
364            _contentId = contentId;
365        }
366        
367        /**
368         * Returns the metadata.
369         * @return the metadata.
370         */
371        public String getMetadata()
372        {
373            return _metadata;
374        }
375        
376        /**
377         * Set the metadata.
378         * @param metadata the metadata.
379         */
380        public void setMetadata(String metadata)
381        {
382            _metadata = metadata;
383        }
384        
385        /**
386         * Returns the resource path.
387         * @return the path
388         */
389        public String getPath()
390        {
391            return _path;
392        }
393        
394        /**
395         * Set the resource path.
396         * @param path the path.
397         */
398        public void setPath(String path)
399        {
400            _path = path;
401        }
402        
403        /**
404         * Returns the content version, if any.
405         * @return the content version.
406         */
407        public String getContentVersion()
408        {
409            return _contentVersion;
410        }
411        
412        /**
413         * Set the content version.
414         * @param contentVersion the content version.
415         */
416        public void setContentVersion(String contentVersion)
417        {
418            _contentVersion = contentVersion;
419        }
420        
421        /**
422         * Returns the resolved content, if any.
423         * @return the content.
424         */
425        public Content getContent()
426        {
427            return _content;
428        }
429        
430        /**
431         * Set the content.
432         * @param content the content.
433         */
434        public void setContent(Content content)
435        {
436            _content = content;
437        }
438    }
439}