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