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; 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}