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.nio.file.Path;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031import java.util.zip.ZipOutputStream;
032
033import javax.jcr.Node;
034import javax.jcr.Repository;
035import javax.jcr.RepositoryException;
036import javax.jcr.Session;
037
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.commons.lang3.StringUtils;
042
043import org.ametys.core.util.LambdaUtils;
044import org.ametys.plugins.repository.provider.AbstractRepository;
045import org.ametys.runtime.i18n.I18nizableTextParameter;
046import org.ametys.runtime.i18n.I18nizableText;
047import org.ametys.runtime.plugin.Plugin;
048import org.ametys.runtime.plugin.PluginsManager;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050
051/**
052 * Archives resources in /ametys:plugins
053 */
054public class PluginsArchiver extends AbstractLogEnabled implements Archiver, Serviceable
055{
056    /** Archiver id. */
057    public static final String ID = "plugins";
058    
059    private static final String __PARTIAL_IMPORT_PREFIX = ID + "/";
060    
061    private Repository _repository;
062    private PluginArchiverExtensionPoint _pluginArchiverExtPoint;
063    
064    private ManifestReaderWriter _manifestReaderWriter = new PluginsArchiverManifestReaderWriter();
065    
066    @Override
067    public void service(ServiceManager manager) throws ServiceException
068    {
069        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
070        _pluginArchiverExtPoint = (PluginArchiverExtensionPoint) manager.lookup(PluginArchiverExtensionPoint.ROLE);
071    }
072    
073    @Override
074    public ManifestReaderWriter getManifestReaderWriter()
075    {
076        return _manifestReaderWriter;
077    }
078    
079    private Map<String, String> _getManifestData()
080    {
081        Session session = null;
082        Map<String, Plugin> pluginDefinitions = PluginsManager.getInstance().getAllPlugins();
083        Map<String, String> pluginVersions = new HashMap<>();
084        
085        try
086        {
087            session = _repository.login();
088            
089            Iterator<Node> it = _getPluginNodes(session);
090            while (it.hasNext())
091            {
092                Node pluginNode = it.next();
093                String pluginName = pluginNode.getName();
094                Plugin pluginDef = pluginDefinitions.get(pluginName);
095                if (pluginDef != null)
096                {
097                    String version = pluginDef.getVersion();
098                    pluginVersions.put(pluginName, version != null ? version : "");
099                }
100                else
101                {
102                    // case of non-plugins eg. 'web-explorer'
103                    pluginVersions.put(pluginName, "");
104                }
105            }
106            
107            return pluginVersions;
108        }
109        catch (RepositoryException e)
110        {
111            throw new IllegalArgumentException("Unable to get plugin list", e);
112        }
113        finally
114        {
115            _closeSession(session);
116        }
117    }
118    
119    private class PluginsArchiverManifestReaderWriter implements ManifestReaderWriter
120    {
121        @Override
122        public Object getData()
123        {
124            return _getManifestData();
125        }
126        
127        @SuppressWarnings("synthetic-access")
128        @Override
129        public Stream<PartialImport> toPartialImports(Object data)
130        {
131            return Optional.ofNullable(data)
132                    .filter(Map.class::isInstance)
133                    .map(this::_castToMap)
134                    .map(Map::entrySet)
135                    .orElseGet(() ->
136                    {
137                        getLogger().warn("Unexpected manifest data '{}', we would expect a JSON object with key-value mappings representing the plugin names and their version. The ZIP archive probably comes from a different version of Ametys.", data);
138                        return Collections.emptySet();
139                    })
140                    .stream()
141                    .map(this::_toPartialImport)
142                    .sorted(Comparator.comparing(PartialImport::getKey)); // sort by plugin name (as key is 'plugins/${pluginName}')
143        }
144        
145        private Map<String, String> _castToMap(Object data)
146        {
147            return Map.class.cast(data);
148        }
149        
150        private PartialImport _toPartialImport(Map.Entry<String, String> entry)
151        {
152            String pluginName = entry.getKey(); // name of the plugin
153            String optionalVersion = entry.getValue(); // can be empty
154            String key = __PARTIAL_IMPORT_PREFIX + pluginName;
155            return PartialImport.of(key, _toPartialImportLabel(pluginName, optionalVersion));
156        }
157        
158        private I18nizableText _toPartialImportLabel(String pluginName, String optionalVersion)
159        {
160            // "Plugin 'myplugin' (2.0.0)"
161            String optionalVersionLabel = optionalVersion.isEmpty() ? "" : " (" + optionalVersion + ")";
162            Map<String, I18nizableTextParameter> parameters = Map.of(
163                    "pluginName", new I18nizableText(pluginName),
164                    "optionalVersion", new I18nizableText(optionalVersionLabel)
165            );
166            return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_ARCHIVE_IMPORT_PLUGIN_ARCHIVER_OPTION_ONE_PLUGIN", parameters);
167        }
168    }
169    
170    private static Node _getAllPluginsNode(Session session) throws RepositoryException
171    {
172        return session.getRootNode()
173                .getNode("ametys:root/ametys:plugins");
174    }
175    
176    private static Iterator<Node> _getPluginNodes(Session session) throws RepositoryException
177    {
178        return _getAllPluginsNode(session)
179                .getNodes();
180    }
181    
182    private static void _closeSession(Session session)
183    {
184        if (session != null)
185        {
186            session.logout();
187        }
188    }
189    
190    @Override
191    public void export(ZipOutputStream zos) throws IOException
192    {
193        Session session = null;
194        try
195        {
196            session = _repository.login();
197            
198            _getPluginNodes(session)
199                    .forEachRemaining(LambdaUtils.wrapConsumer(pluginNode -> _exportPlugin(pluginNode, zos)));
200        }
201        catch (Exception e)
202        {
203            throw new IllegalArgumentException("Unable to archive plugins", e);
204        }
205        finally
206        {
207            _closeSession(session);
208        }
209    }
210    
211    private PluginArchiver _retrievePluginArchiver(String pluginName)
212    {
213        PluginArchiver pluginArchiver = _pluginArchiverExtPoint.getExtension(pluginName);
214        if (pluginArchiver == null)
215        {
216            // there's no specific exporter for this plugin, let's fallback to the default export
217            pluginArchiver = _pluginArchiverExtPoint.getExtension(DefaultPluginArchiver.EXTENSION_ID);
218            
219            if (pluginArchiver == null)
220            {
221                throw new IllegalStateException("There should be a '__default' extension to PluginArchiverExtensionPoint. Please check your excluded features.");
222            }
223        }
224        
225        return pluginArchiver;
226    }
227    
228    private void _exportPlugin(Node pluginNode, ZipOutputStream zos) throws Exception
229    {
230        String pluginName = pluginNode.getName();
231        PluginArchiver pluginArchiver = _retrievePluginArchiver(pluginName);
232        getLogger().info("Processing plugin '{}' for archiving at {} with archiver '{}'", pluginName, pluginNode, pluginArchiver.getClass().getName());
233        pluginArchiver.export(pluginName, pluginNode, zos, "plugins/" + pluginName);
234    }
235    
236    @Override
237    public Collection<String> managedPartialImports(Collection<String> partialImports)
238    {
239        return partialImports
240                .stream()
241                .filter(partialImport -> partialImport.startsWith(__PARTIAL_IMPORT_PREFIX))
242                .collect(Collectors.toList());
243    }
244    
245    @Override
246    public ImportReport partialImport(Path zipPath, Collection<String> partialImports, Merger merger, boolean deleteBefore) throws IOException
247    {
248        List<ImportReport> reports = new ArrayList<>();
249        
250        Session session = null;
251        try
252        {
253            session = _repository.login();
254            Node pluginsNode = _getAllPluginsNode(session);
255            for (String partialImport : partialImports)
256            {
257                ImportReport pluginImportResult = _importPlugin(partialImport, zipPath, merger, pluginsNode, deleteBefore);
258                reports.add(pluginImportResult);
259            }
260            
261            return ImportReport.union(reports);
262        }
263        catch (RepositoryException e)
264        {
265            throw new IllegalArgumentException("Unable to import plugins", e);
266        }
267        finally
268        {
269            _closeSession(session);
270        }
271    }
272    
273    private ImportReport _importPlugin(String partialImport, Path zipPath, Merger merger, Node allPluginsNode, boolean deleteBefore) throws IOException, RepositoryException
274    {
275        String pluginName = StringUtils.substringAfter(partialImport, __PARTIAL_IMPORT_PREFIX);
276        PluginArchiver pluginArchiver = _retrievePluginArchiver(pluginName);
277        getLogger().info("Importing plugin '{}' from archive with archiver '{}'", pluginName, pluginArchiver.getClass().getName());
278        if (deleteBefore)
279        {
280            pluginArchiver.deleteBeforePartialImport(pluginName, allPluginsNode);
281        }
282        
283        String zipPluginEntryPath = "plugins/" + pluginName;
284        return pluginArchiver.partialImport(pluginName, allPluginsNode, zipPath, zipPluginEntryPath, merger);
285    }
286}