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}