001/*
002 *  Copyright 2014 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.contentio.in;
017
018import java.io.BufferedInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Map;
030import java.util.Set;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.commons.io.FileUtils;
037import org.apache.commons.io.FilenameUtils;
038import org.apache.commons.io.IOUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.excalibur.xml.dom.DOMParser;
041import org.w3c.dom.Document;
042import org.xml.sax.InputSource;
043
044import org.ametys.plugins.contentio.in.xml.XmlContentImporter;
045import org.ametys.runtime.plugin.component.AbstractLogEnabled;
046import org.ametys.runtime.util.AmetysHomeHelper;
047
048/**
049 * Component handling the import of contents, based on the {@link ContentImporter} multiple extension point.
050 */
051public class ContentImportManager extends AbstractLogEnabled implements Serviceable, Component
052{
053    
054    /** The avalon role. */
055    public static final String ROLE = ContentImportManager.class.getName();
056    
057    /** The content importer extension point. */
058    protected ContentImporterExtensionPoint _contentImportEP;
059    
060    /** A DOM parser. */
061    protected DOMParser _domParser;
062    
063    @Override
064    public void service(ServiceManager serviceManager) throws ServiceException
065    {
066        _contentImportEP = (ContentImporterExtensionPoint) serviceManager.lookup(ContentImporterExtensionPoint.ROLE);
067        _domParser = (DOMParser) serviceManager.lookup(DOMParser.ROLE);
068    }
069    
070    /**
071     * Import contents from a given file.
072     * @param file the file describing the contents to import.
073     * @return a Set of imported content IDs.
074     * @throws ContentImportException if an exception occurs while importing the contents from the file.
075     */
076    public ImportResult importContents(File file) throws ContentImportException
077    {
078        String name = file.getName();
079        
080        if (!file.canRead() || !file.isFile())
081        {
082            throw new ContentImportException("The file " + name + " is not a regular file or can't be read.");
083        }
084
085        try (InputStream is = new FileInputStream(file);)
086        {
087            
088            ContentImporter importer = getImporter(file);
089            
090            if (importer != null)
091            {
092                HashMap<String, Object> params = new HashMap<>();
093                Set<String> contentIds = importer.importContents(is, params);
094                
095                return new ImportResult(contentIds);
096            }
097            else
098            {
099                return new ImportResult(false);
100            }
101        }
102        catch (IOException e)
103        {
104            getLogger().error("IO error while importing a content from file {}", name, e);
105            throw new ContentImportException("IO error while importing a content from file " + name, e);
106        }
107    }
108    
109    /**
110     * Import contents from an input stream.
111     * @param is an input stream on the data describing the contents to import.
112     * @param name the "file" name, can be null. If it's null, some importers may not be able to identify the stream.
113     * @return a Set of imported content IDs.
114     * @throws ContentImportException if an exception occurs while importing the contents from the file.
115     */
116    public ImportResult importContents(InputStream is, String name) throws ContentImportException
117    {
118        File tempFile = null;
119        
120        try
121        {
122            // Create a temporary file from the input stream, to be able to read it more than once.
123            if (StringUtils.isNotEmpty(name))
124            {
125                String baseName = FilenameUtils.getBaseName(name);
126                String ext = FilenameUtils.getExtension(name);
127                tempFile = File.createTempFile(baseName, ext, AmetysHomeHelper.getAmetysHomeTmp());
128            }
129            else
130            {
131                tempFile = File.createTempFile("content-import-", null, AmetysHomeHelper.getAmetysHomeTmp());
132            }
133            
134            // Copy the stream to the temporary file.
135            try (OutputStream os = new FileOutputStream(tempFile))
136            {
137                IOUtils.copy(is, os);
138            }
139            
140            // Import contents from the temporary file.
141            return importContents(tempFile);
142        }
143        catch (IOException e)
144        {
145            getLogger().error("IO error while importing a content from file {}", name, e);
146            throw new ContentImportException("IO error while importing a content from file " + name, e);
147        }
148        finally
149        {
150            FileUtils.deleteQuietly(tempFile);
151        }
152    }
153    
154    /**
155     * Get the {@link ContentImporter} supporting the file.
156     * @param file the file
157     * @return the {@link ContentImporter} corresponding to a 
158     * @throws IOException if an exception occurs when manipulating the file
159     */
160    protected ContentImporter getImporter(File file) throws IOException
161    {
162        String name = file.getName();
163        
164        // Maintain an import context map, which can be reused across the calls to supports.
165        Map<String, Object> importContext = new HashMap<>();
166        
167        // Browse importers ordered by priority: the first matching will be the one having most priority.
168        for (ContentImporter importer : _contentImportEP.getImportersByPriority())
169        {
170            try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));)
171            {
172                if (supports(importer, in, name, importContext))
173                {
174                    return importer;
175                }
176            }
177        }
178        
179        return null;
180    }
181    
182    /**
183     * Test if an importer supports the file.
184     * @param importer the importer to test.
185     * @param in an InputStream on the file.
186     * @param name the file name, can be null.
187     * @param importContext the import context map.
188     * @return true if the importer supports the file, false otherwise.
189     * @throws IOException if an error occurs testing the importer.
190     */
191    protected boolean supports(ContentImporter importer, BufferedInputStream in, String name, Map<String, Object> importContext) throws IOException
192    {
193        // Specific test for XML content importers.
194        if (importer instanceof XmlContentImporter)
195        {
196            return supportsXml((XmlContentImporter) importer, in, importContext);
197        }
198        
199        // Standard test.
200        return importer.supports(in, name);
201    }
202    
203    /**
204     * Test if an {@link XmlContentImporter} supports the file.
205     * @param importer the XML content importer to test.
206     * @param in an InputStream on the file.
207     * @param importContext the import context map.
208     * @return true if the importer supports the file, false otherwise.
209     * @throws IOException if an error occurs testing the importer.
210     */
211    protected boolean supportsXml(XmlContentImporter importer, BufferedInputStream in, Map<String, Object> importContext) throws IOException
212    {
213        Document document = null;
214        
215        // Store the parsing status and result in the import context, to read the XML only once.
216        if (!importContext.containsKey("xml-document"))
217        {
218            try
219            {
220                document = _domParser.parseDocument(new InputSource(in));
221            }
222            catch (Exception e)
223            {
224                // This is not a valid XML document: the document variable will remain null
225                // and we won't try to parse the document again.
226            }
227            
228            importContext.put("xml-document", document);
229        }
230        else
231        {
232            document = (Document) importContext.get("xml-document");
233        }
234        
235        // If the document could be parsed, test if the importer supports it.
236        if (document != null)
237        {
238            return importer.supports(document);
239        }
240        
241        return false;
242    }
243    
244    /**
245     * Class representing a content import result.
246     */
247    public class ImportResult
248    {
249        
250        /** If an importer supporting the file has been found. */
251        protected boolean _importerFound;
252        
253        /** The list of imported content IDs. */
254        protected Set<String> _importedContentIds;
255        
256        /**
257         * Build an ImportResult.
258         * @param importerFound true if an importer was found, false otherwise.
259         */
260        public ImportResult(boolean importerFound)
261        {
262            this._importerFound = importerFound;
263            this._importedContentIds = Collections.emptySet();
264        }
265        
266        /**
267         * Build an ImportResult.
268         * @param importedContentIds the imported content IDs.
269         */
270        public ImportResult(Collection<String> importedContentIds)
271        {
272            this._importerFound = true;
273            this._importedContentIds = new HashSet<>(importedContentIds);
274        }
275        
276        /**
277         * Get the importerFound.
278         * @return the importerFound
279         */
280        public boolean isImporterFound()
281        {
282            return _importerFound;
283        }
284        
285        /**
286         * Set the importerFound.
287         * @param importerFound the importerFound to set
288         */
289        public void setImporterFound(boolean importerFound)
290        {
291            this._importerFound = importerFound;
292        }
293        
294        /**
295         * Get the importedContentIds.
296         * @return the importedContentIds
297         */
298        public Set<String> getImportedContentIds()
299        {
300            return _importedContentIds;
301        }
302        
303        /**
304         * Set the importedContentIds.
305         * @param importedContentIds the importedContentIds to set
306         */
307        public void setImportedContentIds(Collection<String> importedContentIds)
308        {
309            this._importedContentIds = new HashSet<>(importedContentIds);
310        }
311        
312    }
313    
314}