001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
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 *
016 * Contributors:
017 *     <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
018 *
019 */
020
021package org.nuxeo.ecm.platform.io.impl;
022
023import java.io.ByteArrayOutputStream;
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.FileOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.OutputStream;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.zip.ZipEntry;
036import java.util.zip.ZipException;
037import java.util.zip.ZipInputStream;
038import java.util.zip.ZipOutputStream;
039
040import org.apache.commons.io.IOUtils;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.core.api.CoreInstance;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentLocation;
046import org.nuxeo.ecm.core.api.DocumentModel;
047import org.nuxeo.ecm.core.api.DocumentRef;
048import org.nuxeo.ecm.core.api.DocumentTreeIterator;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.ecm.core.io.DocumentReader;
051import org.nuxeo.ecm.core.io.DocumentReaderFactory;
052import org.nuxeo.ecm.core.io.DocumentTranslationMap;
053import org.nuxeo.ecm.core.io.DocumentWriter;
054import org.nuxeo.ecm.core.io.DocumentWriterFactory;
055import org.nuxeo.ecm.core.io.DocumentsExporter;
056import org.nuxeo.ecm.core.io.DocumentsImporter;
057import org.nuxeo.ecm.core.io.IODocumentManager;
058import org.nuxeo.ecm.core.io.impl.DocumentTranslationMapImpl;
059import org.nuxeo.ecm.core.io.impl.IODocumentManagerImpl;
060import org.nuxeo.ecm.platform.io.api.IOManager;
061import org.nuxeo.ecm.platform.io.api.IOResourceAdapter;
062import org.nuxeo.ecm.platform.io.api.IOResources;
063import org.nuxeo.runtime.api.Framework;
064
065/**
066 * IOManager implementation
067 *
068 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
069 */
070public class IOManagerImpl implements IOManager {
071
072    private static final long serialVersionUID = 5789086884484295921L;
073
074    private static final Log log = LogFactory.getLog(IOManagerImpl.class);
075
076    protected final Map<String, IOResourceAdapter> adaptersRegistry;
077
078    public IOManagerImpl() {
079        adaptersRegistry = new HashMap<>();
080    }
081
082    @Override
083    public IOResourceAdapter getAdapter(String name) {
084        return adaptersRegistry.get(name);
085    }
086
087    @Override
088    public void addAdapter(String name, IOResourceAdapter adapter) {
089        if (DOCUMENTS_ADAPTER_NAME.equals(name)) {
090            log.error("Cannot register adapter with name " + DOCUMENTS_ADAPTER_NAME);
091            return;
092        }
093        adaptersRegistry.put(name, adapter);
094    }
095
096    @Override
097    public void removeAdapter(String name) {
098        adaptersRegistry.remove(name);
099    }
100
101    public void exportDocumentsAndResources(OutputStream out, String repo, final String format,
102            Collection<String> ioAdapters, final DocumentReader customDocReader) throws IOException {
103        DocumentsExporter docsExporter = new DocumentsExporter() {
104            @Override
105            public DocumentTranslationMap exportDocs(OutputStream out) throws IOException {
106                IODocumentManager docManager = new IODocumentManagerImpl();
107                DocumentTranslationMap map = docManager.exportDocuments(out, customDocReader, format);
108                return map;
109            }
110        };
111        exportDocumentsAndResources(out, repo, docsExporter, ioAdapters);
112    }
113
114    @Override
115    public void exportDocumentsAndResources(OutputStream out, final String repo, final Collection<DocumentRef> sources,
116            final boolean recurse, final String format, final Collection<String> ioAdapters) throws IOException {
117        DocumentsExporter docsExporter = new DocumentsExporter() {
118            @Override
119            public DocumentTranslationMap exportDocs(OutputStream out) throws IOException {
120                IODocumentManager docManager = new IODocumentManagerImpl();
121                DocumentTranslationMap map = docManager.exportDocuments(out, repo, sources, recurse, format);
122                return map;
123            }
124        };
125        exportDocumentsAndResources(out, repo, docsExporter, ioAdapters);
126    }
127
128    void exportDocumentsAndResources(OutputStream out, String repo, DocumentsExporter docsExporter,
129            Collection<String> ioAdapters) throws IOException {
130        List<String> doneAdapters = new ArrayList<>();
131
132        ZipOutputStream zip = new ZipOutputStream(out);
133        zip.setMethod(ZipOutputStream.DEFLATED);
134        zip.setLevel(9);
135
136        ByteArrayOutputStream docsZip = new ByteArrayOutputStream();
137        DocumentTranslationMap map = docsExporter.exportDocs(docsZip);
138
139        ZipEntry docsEntry = new ZipEntry(DOCUMENTS_ADAPTER_NAME + ".zip");
140        zip.putNextEntry(docsEntry);
141        zip.write(docsZip.toByteArray());
142        zip.closeEntry();
143        docsZip.close();
144        doneAdapters.add(DOCUMENTS_ADAPTER_NAME);
145
146        Collection<DocumentRef> allSources = map.getDocRefMap().keySet();
147
148        if (ioAdapters != null && !ioAdapters.isEmpty()) {
149            for (String adapterName : ioAdapters) {
150                String filename = adapterName + ".xml";
151                IOResourceAdapter adapter = getAdapter(adapterName);
152                if (adapter == null) {
153                    log.warn("Adapter " + adapterName + " not found");
154                    continue;
155                }
156                if (doneAdapters.contains(adapterName)) {
157                    log.warn("Export for adapter " + adapterName + " already done");
158                    continue;
159                }
160                IOResources resources = adapter.extractResources(repo, allSources);
161                resources = adapter.translateResources(repo, resources, map);
162                ByteArrayOutputStream adapterOut = new ByteArrayOutputStream();
163                adapter.getResourcesAsXML(adapterOut, resources);
164                ZipEntry adapterEntry = new ZipEntry(filename);
165                zip.putNextEntry(adapterEntry);
166                zip.write(adapterOut.toByteArray());
167                zip.closeEntry();
168                doneAdapters.add(adapterName);
169                adapterOut.close();
170            }
171        }
172        try {
173            zip.close();
174        } catch (ZipException e) {
175            // empty zip file, do nothing
176        }
177    }
178
179    @Override
180    public void importDocumentsAndResources(InputStream in, final String repo, final DocumentRef root)
181            throws IOException {
182        DocumentsImporter docsImporter = new DocumentsImporter() {
183
184            @Override
185            public DocumentTranslationMap importDocs(InputStream sourceStream) throws IOException {
186                IODocumentManager docManager = new IODocumentManagerImpl();
187                return docManager.importDocuments(sourceStream, repo, root);
188            }
189
190        };
191        importDocumentsAndResources(docsImporter, in, repo);
192    }
193
194    public void importDocumentsAndResources(InputStream in, final String repo, final DocumentRef root,
195            final DocumentWriter customDocWriter) throws IOException {
196        DocumentsImporter docsImporter = new DocumentsImporter() {
197
198            @Override
199            public DocumentTranslationMap importDocs(InputStream sourceStream) throws IOException {
200                IODocumentManager docManager = new IODocumentManagerImpl();
201                return docManager.importDocuments(sourceStream, customDocWriter);
202            }
203
204        };
205        importDocumentsAndResources(docsImporter, in, repo);
206    }
207
208    void importDocumentsAndResources(DocumentsImporter docsImporter, InputStream in, String repo) throws IOException {
209        ZipInputStream zip = new ZipInputStream(in);
210
211        // first entry will be documents
212        ZipEntry zentry = zip.getNextEntry();
213        String docZipFilename = DOCUMENTS_ADAPTER_NAME + ".zip";
214        if (zentry == null || !docZipFilename.equals(zentry.getName())) {
215            zip.close();
216            throw new NuxeoException("Invalid archive");
217        }
218
219        // fill in a new stream
220        File temp = Framework.createTempFile("nuxeo-import-adapters-", ".zip");
221        try (FileOutputStream outDocs = new FileOutputStream(temp)) {
222            IOUtils.copy(zip, outDocs);
223        }
224        zip.closeEntry();
225
226        InputStream tempIn = new FileInputStream(temp.getPath());
227        DocumentTranslationMap map = docsImporter.importDocs(tempIn);
228        tempIn.close();
229        temp.delete();
230
231        while ((zentry = zip.getNextEntry()) != null) {
232            String entryName = zentry.getName();
233            if (entryName.endsWith(".xml")) {
234                String ioAdapterName = entryName.substring(0, entryName.length() - 4);
235                IOResourceAdapter adapter = getAdapter(ioAdapterName);
236                if (adapter == null) {
237                    log.warn("Adapter " + ioAdapterName + " not available. Unable to import associated resources.");
238                    continue;
239                }
240                IOResources resources = adapter.loadResourcesFromXML(zip);
241                IOResources newResources = adapter.translateResources(repo, resources, map);
242                log.info("store resources with adapter " + ioAdapterName);
243                adapter.storeResources(newResources);
244            } else {
245                log.warn("skip entry: " + entryName);
246            }
247            try {
248                // we might have an undesired stream close in the client
249                zip.closeEntry();
250            } catch (IOException e) {
251                log.error("Please check code handling entry " + entryName, e);
252            }
253        }
254        zip.close();
255    }
256
257    @Override
258    public Collection<DocumentRef> copyDocumentsAndResources(String repo, Collection<DocumentRef> sources,
259            DocumentLocation targetLocation, Collection<String> ioAdapters) {
260        if (sources == null || sources.isEmpty()) {
261            return null;
262        }
263
264        String newRepo = targetLocation.getServerName();
265        if (!repo.equals(newRepo)) {
266            // TODO: maybe import and export (?), assume copy is recursive.
267            throw new NuxeoException("Cannot copy to different server");
268        }
269
270        List<DocumentRef> roots = new ArrayList<>();
271        try (CoreSession session = CoreInstance.openCoreSession(repo)) {
272            for (DocumentRef source : sources) {
273                DocumentTranslationMap map = new DocumentTranslationMapImpl(repo, repo);
274                DocumentModel sourceDoc = session.getDocument(source);
275                DocumentModel destDoc = session.copy(source, targetLocation.getDocRef(), null);
276                roots.add(destDoc.getRef());
277                // iterate on each tree to build translation map
278                DocumentTreeIterator sourceIt = new DocumentTreeIterator(session, sourceDoc);
279                DocumentTreeIterator destIt = new DocumentTreeIterator(session, destDoc);
280                while (sourceIt.hasNext()) {
281                    DocumentModel sourceItem = sourceIt.next();
282                    DocumentRef sourceRef = sourceItem.getRef();
283                    if (!destIt.hasNext()) {
284                        map.put(sourceRef, null);
285                    } else {
286                        DocumentModel destItem = destIt.next();
287                        DocumentRef destRef = destItem.getRef();
288                        map.put(sourceRef, destRef);
289                    }
290                }
291                Collection<DocumentRef> allSources = map.getDocRefMap().keySet();
292                if (ioAdapters != null && !ioAdapters.isEmpty()) {
293                    for (String adapterName : ioAdapters) {
294                        IOResourceAdapter adapter = getAdapter(adapterName);
295                        if (adapter == null) {
296                            log.warn("Adapter " + adapterName + " not found");
297                            continue;
298                        }
299                        IOResources resources = adapter.extractResources(repo, allSources);
300                        IOResources newResources = adapter.translateResources(repo, resources, map);
301                        adapter.storeResources(newResources);
302                    }
303                }
304                session.save();
305            }
306        }
307        return roots;
308    }
309
310    private static DocumentWriter createDocWriter(String docWriterFactoryName, Map<String, Object> factoryParams) {
311        // create a custom writer using factory instance
312        Object factoryObj;
313        try {
314            Class<?> clazz = Class.forName(docWriterFactoryName);
315            factoryObj = clazz.newInstance();
316        } catch (ReflectiveOperationException e) {
317            throw new NuxeoException("cannot instantiate factory " + docWriterFactoryName, e);
318        }
319
320        DocumentWriter customDocWriter;
321        if (factoryObj instanceof DocumentWriterFactory) {
322            customDocWriter = ((DocumentWriterFactory) factoryObj).createDocWriter(factoryParams);
323        } else {
324            throw new NuxeoException("bad class type: " + factoryObj);
325        }
326
327        if (customDocWriter == null) {
328            throw new NuxeoException("null DocumentWriter created by " + docWriterFactoryName);
329        }
330
331        return customDocWriter;
332    }
333
334    private static DocumentReader createDocReader(String docReaderFactoryName, Map<String, Object> factoryParams) {
335        // create a custom reader using factory instance
336        Object factoryObj;
337        try {
338            Class<?> clazz = Class.forName(docReaderFactoryName);
339            factoryObj = clazz.newInstance();
340        } catch (ReflectiveOperationException e) {
341            throw new NuxeoException("cannot instantiate factory " + docReaderFactoryName, e);
342        }
343
344        DocumentReader customDocReader;
345        if (factoryObj instanceof DocumentReaderFactory) {
346            customDocReader = ((DocumentReaderFactory) factoryObj).createDocReader(factoryParams);
347        } else {
348            throw new NuxeoException("bad class type: " + factoryObj);
349        }
350
351        if (customDocReader == null) {
352            throw new NuxeoException("null DocumentReader created by " + docReaderFactoryName);
353        }
354
355        return customDocReader;
356    }
357
358    @Override
359    public void importFromStream(InputStream in, DocumentLocation targetLocation, String docReaderFactoryClassName,
360            Map<String, Object> rFactoryParams, String docWriterFactoryClassName, Map<String, Object> wFactoryParams) {
361        DocumentWriter customDocWriter = createDocWriter(docWriterFactoryClassName, wFactoryParams);
362        DocumentReader customDocReader = null;
363
364        try {
365            if (rFactoryParams == null) {
366                rFactoryParams = new HashMap<>();
367            }
368            rFactoryParams.put("source_stream", in);
369            customDocReader = createDocReader(docReaderFactoryClassName, rFactoryParams);
370
371            IODocumentManager docManager = new IODocumentManagerImpl();
372            DocumentTranslationMap map = docManager.importDocuments(customDocReader, customDocWriter);
373        } finally {
374            if (customDocReader != null) {
375                customDocReader.close();
376            }
377            customDocWriter.close();
378
379            if (in != null) {
380                try {
381                    in.close();
382                } catch (IOException e) {
383                    log.error(e);
384                }
385            }
386        }
387
388    }
389
390}