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}