001/*
002 *  Copyright 2019 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.archive;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.nio.file.Path;
021import java.util.Collections;
022import java.util.zip.ZipEntry;
023import java.util.zip.ZipOutputStream;
024
025import javax.jcr.Node;
026import javax.jcr.RepositoryException;
027import javax.jcr.Session;
028import javax.xml.transform.sax.TransformerHandler;
029import javax.xml.transform.stream.StreamResult;
030
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.xml.sax.ContentHandler;
035
036import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint;
037import org.ametys.plugins.repository.RepositoryConstants;
038import org.ametys.plugins.repository.collection.AmetysObjectCollection;
039import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory;
040import org.ametys.runtime.plugin.component.AbstractLogEnabled;
041
042/**
043 * Default implementation of a {@link PluginArchiver}. It uses the JCR system view for all data but contents.
044 * For contents, the {@link ContentsArchiverHelper} is used.
045 */
046public class DefaultPluginArchiver extends AbstractLogEnabled implements PluginArchiver, Serviceable
047{
048    /** Id for default implementation */
049    public static final String EXTENSION_ID = "__default";
050    
051    private static final String __CONTENT_ROOT_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":contents";
052    
053    private ContentsArchiverHelper _contentsArchiverHelper;
054    private AmetysObjectCollectionFactory _ametysObjectCollectionFactory;
055    
056    @Override
057    public void service(ServiceManager manager) throws ServiceException
058    {
059        _contentsArchiverHelper = (ContentsArchiverHelper) manager.lookup(ContentsArchiverHelper.ROLE);
060        AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE);
061        _ametysObjectCollectionFactory = (AmetysObjectCollectionFactory) ametysObjectFactoryEP.getExtension(AmetysObjectCollectionFactory.class.getName());
062    }
063
064    @Override
065    public void export(String pluginName, Node pluginNode, ZipOutputStream zos, String prefix) throws IOException
066    {
067        // first export non-content data with the JCR system view
068        ZipEntry pluginEntry = new ZipEntry(prefix + "/plugin.xml");
069        zos.putNextEntry(pluginEntry);
070        
071        try
072        {
073            TransformerHandler handler = Archivers.newTransformerHandler();
074            handler.setResult(new StreamResult(zos));
075            
076            pluginNode.getSession().exportSystemView(pluginNode.getPath(), getSystemViewHandler(handler), false, false);
077            
078            // then the contents if any, one by one
079            if (pluginNode.hasNode(__CONTENT_ROOT_NODE_NAME))
080            {
081                _contentsArchiverHelper.exportContents(prefix + "/contents/", pluginNode.getNode("ametys:contents"), zos);
082            }
083        }
084        catch (Exception e)
085        {
086            throw new RuntimeException("Unable to archive plugin " + pluginName, e);
087        }
088    }
089    
090    /**
091     * Returns the actual handler receiving the JCR system view. May be used to filter out some parts of the JCR export.
092     * @param initialHandler the target {@link ContentHandler}.
093     * @return a ContentHandler.
094     */
095    protected ContentHandler getSystemViewHandler(ContentHandler initialHandler)
096    {
097        return new SystemViewHandler(initialHandler, name -> "ametys:contents".equals(name), __ -> false);
098    }
099    
100    @Override
101    public ImportReport partialImport(String pluginName, Node allPluginsNode, Path zipPath, String zipPluginEntryPath, Merger merger) throws IOException
102    {
103        _importPluginXml(allPluginsNode, zipPath, zipPluginEntryPath, merger);
104        ImportReport importContentReport = importContentsIfAny(pluginName, allPluginsNode, zipPath, zipPluginEntryPath, merger);
105        return ImportReport.union(importContentReport);
106    }
107    
108    private void _importPluginXml(Node allPluginsNode, Path zipPath, String zipPluginEntryPath, Merger merger) throws IOException
109    {
110        String zipPluginXmlEntryPath = zipPluginEntryPath + "/plugin.xml";
111        try (InputStream in = ZipEntryHelper.zipEntryFileInputStream(zipPath, zipPluginXmlEntryPath))
112        {
113            Session session = allPluginsNode.getSession();
114            String parentAbsPath = allPluginsNode.getPath();
115            getLogger().info("XML from '{}!{}' will be imported to '{}' with implementation of merger '{}'", zipPath, zipPluginXmlEntryPath, parentAbsPath, merger);
116            merger.jcrImportXml(session, parentAbsPath, in);
117            session.save();
118        }
119        catch (RepositoryException e)
120        {
121            throw new IOException(e);
122        }
123    }
124    
125    /**
126     * Import some contents if there is a folder named 'contents'
127     * @param pluginName the plugin name.
128     * @param allPluginsNode the {@link Node} for all plugins.
129     * @param zipPath The input ZIP file
130     * @param zipPluginEntryPath The input ZIP entry
131     * @param merger The {@link Merger}
132     * @return The {@link ImportReport}
133     * @throws IOException if an error occurs while reading the archive.
134     */
135    protected ImportReport importContentsIfAny(String pluginName, Node allPluginsNode, Path zipPath, String zipPluginEntryPath, Merger merger) throws IOException
136    {
137        String baseImportContentPath = zipPluginEntryPath + "/contents";
138        if (ZipEntryHelper.zipEntryFolderExists(zipPath, baseImportContentPath))
139        {
140            try
141            {
142                // it must exist as #_importPluginXml must be called before
143                Node pluginNode = allPluginsNode.getNode(pluginName);
144                
145                String pluginNodePath = pluginNode.getPath();
146                AmetysObjectCollection rootContents = pluginNode.hasNode(__CONTENT_ROOT_NODE_NAME)
147                        ? _ametysObjectCollectionFactory.getObject(pluginNodePath, pluginNode.getNode(__CONTENT_ROOT_NODE_NAME), pluginNodePath)
148                        : _ametysObjectCollectionFactory.createChild(pluginNodePath, pluginNode, __CONTENT_ROOT_NODE_NAME, "ametys:collection");
149                return _contentsArchiverHelper.importContents(baseImportContentPath + "/", rootContents, zipPath, merger, Collections.EMPTY_SET);
150            }
151            catch (RepositoryException e)
152            {
153                throw new IOException(e);
154            }
155        }
156        else
157        {
158            getLogger().info("No content to be imported for Plugin '{}', the path '{}!{}' does not exist", pluginName, zipPath, baseImportContentPath);
159            return new ImportReport();
160        }
161    }
162}