001/*
002 *  Copyright 2010 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        // uri are like content://UUID@metadata;data/file.ext
081        int i = uri.indexOf('@');
082        int j = uri.indexOf(';', i);
083        String id = uri.substring(0, i);
084        String metadata = uri.substring(i + 1, j);
085        String path = uri.substring(j + 1);
086
087        try
088        {
089            Request request = ContextHelper.getRequest(_context);
090            
091            String contentVersion = null;
092            Content content = (Content) request.getAttribute(Content.class.getName());
093            if (content != null && id.equals(content.getId()))
094            {
095                contentVersion = ((VersionableAmetysObject) content).getRevision();
096            }
097            
098            StringBuilder resultPath = new StringBuilder();
099            
100            resultPath.append(getUriPrefix(download, absolute, internal))
101                .append("/plugins/cms" + (download ? "/download/" : "/view/"))
102                .append(path);
103            
104            Map<String, String> params = new HashMap<>();
105            params.put("contentId", id);
106            params.put("metadata", metadata);
107            if (contentVersion != null)
108            {
109                params.put("contentVersion", contentVersion);
110            }
111            
112            // Encode twice
113            String encodedPath = URLEncoder.encodePath(resultPath.toString());
114            return URLEncoder.encodeURI(encodedPath, params);
115        }
116        catch (Exception e)
117        {
118            throw new IllegalStateException(e);
119        }
120    }
121    
122    /**
123     * Get the URI prefix
124     * @param download true if the pointed resource is to be downloaded.
125     * @param absolute true if the url must be absolute
126     * @param internal true to get an internal URI.
127     * @return the URI prefix
128     */
129    protected String getUriPrefix (boolean download, boolean absolute, boolean internal)
130    {
131        if (internal)
132        {
133            return "cocoon://";
134        }
135        else 
136        {
137            Request request = ContextHelper.getRequest(_context);
138            String workspaceURI = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI);
139            String uriPrefix = request.getContextPath() + workspaceURI;
140            
141            if (absolute && !uriPrefix.startsWith(request.getScheme()))
142            {
143                uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix;
144            }
145            
146            return uriPrefix;
147        }
148    }
149    
150    @Override
151    public String resolveImage(String uri, int height, int width, boolean download, boolean absolute, boolean internal)
152    {
153        return resolve(uri, download, absolute, internal);
154    }
155    
156    @Override
157    public String resolveImageAsBase64(String uri, int height, int width)
158    {
159        return resolveImageAsBase64(uri, height, width, 0, 0);
160    }
161    
162    @Override
163    public String resolveBoundedImage(String uri, int maxHeight, int maxWidth, boolean download, boolean absolute, boolean internal)
164    {
165        return resolve(uri, download, absolute, internal);
166    }
167    
168    @Override
169    public String resolveBoundedImageAsBase64(String uri, int maxHeight, int maxWidth)
170    {
171        return resolveImageAsBase64(uri, 0, 0, maxHeight, maxWidth);
172    }
173    
174    /**
175     * Get an image's bytes encoded as base64, optionally resized. 
176     * @param uri the image URI.
177     * @param height the specified height. Ignored if negative.
178     * @param width the specified width. Ignored if negative.
179     * @param maxHeight the maximum image height. Ignored if height or width is specified.
180     * @param maxWidth the maximum image width. Ignored if height or width is specified.
181     * @return the image bytes encoded as base64.
182     */
183    protected String resolveImageAsBase64(String uri, int height, int width, int maxHeight, int maxWidth)
184    {
185        // uri are like content://UUID@metadata;data/file.ext
186        int i = uri.indexOf('@');
187        int j = uri.indexOf(';', i);
188        String id = uri.substring(0, i);
189        String metadata = uri.substring(i + 1, j);
190        String path = uri.substring(j + 1);
191        
192        try
193        {
194            Request request = ContextHelper.getRequest(_context);
195            
196            Content content = (Content) request.getAttribute(Content.class.getName());
197            if (!content.getId().equals(id))
198            {
199                content = _ametysObjectResolver.resolveById(id);
200            }
201            
202            RichText richText = _getMeta(content.getMetadataHolder(), metadata);
203            File file = _getFile(richText.getAdditionalDataFolder(), path);
204            Resource resource = file.getResource();
205            
206            try (InputStream dataIs = resource.getInputStream())
207            {
208                return ImageResolverHelper.resolveImageAsBase64(dataIs, resource.getMimeType(), height, width, maxHeight, maxWidth);
209            }
210        }
211        catch (Exception e)
212        {
213            throw new IllegalStateException(e);
214        }
215    }
216    
217    @Override
218    public CHECK checkLink(String uri, boolean shortTest)
219    {
220        try
221        {
222            int i = uri.indexOf('@');
223            int j = uri.indexOf(';', i);
224            
225            if (i == -1 || j == -1)
226            {
227                getLogger().warn("Failed to check local URI: '" + uri + " does not respect the excepted format 'content://UUID@metadata;data/file.ext'");
228                return CHECK.SERVER_ERROR;
229            }
230            
231            String id = uri.substring(0, i);
232            String metadata = uri.substring(i + 1, j);
233            String fileName = uri.substring(j + 1);
234            
235            Content content = _ametysObjectResolver.resolveById(id);
236            RichText richText = _getMeta(content.getMetadataHolder(), metadata);
237            
238            richText.getAdditionalDataFolder().getFile(fileName);
239            
240            return CHECK.SUCCESS;
241        }
242        catch (UnknownMetadataException e)
243        {
244            return CHECK.NOT_FOUND;
245        }
246        catch (Exception e)
247        {
248            throw new RuntimeException("Cannot check the uri '" + uri + "'", e);
249        }
250    }
251    
252    @Override
253    public I18nizableText getLabel(String uri)
254    {
255        int i = uri.indexOf('@');
256        int j = uri.indexOf(';', i);
257        
258        String fileName = uri.substring(j + 1);
259        
260        return new I18nizableText("plugin.cms", "PLUGINS_CMS_LINK_LOCAL_LABEL", Collections.singletonList(fileName));
261    }
262    
263    /**
264     * Get the rich text meta
265     * @param meta The composite meta
266     * @param metadataName The metadata name (with /)
267     * @return The rich text meta
268     */
269    protected RichText _getMeta(CompositeMetadata meta, String metadataName)
270    {
271        int pos = metadataName.indexOf("/");
272        if (pos == -1)
273        {
274            return meta.getRichText(metadataName);
275        }
276        else
277        {
278            return _getMeta(meta.getCompositeMetadata(metadataName.substring(0, pos)), metadataName.substring(pos + 1));
279        }
280    }
281    
282    /**
283     * Get the file at the specified path in the given folder.
284     * @param folder The folder to search in.
285     * @param path The file path in the folder (can contain slashes).
286     * @return The file if found, null otherwise.
287     */
288    protected File _getFile(Folder folder, String path)
289    {
290        File file = null;
291        
292        Iterator<String> it = Arrays.asList(path.split("/")).iterator();
293        
294        Folder browsedFolder = folder;
295        while (it.hasNext())
296        {
297            String pathElement = it.next();
298            
299            if (it.hasNext())
300            {
301                // not the last segment : it is a composite
302                browsedFolder = browsedFolder.getFolder(pathElement);
303            }
304            else
305            {
306                file = browsedFolder.getFile(pathElement);
307            }
308        }
309        
310        return file;
311    }
312}