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