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