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}