001/* 002 * Copyright 2023 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 */ 016 017package org.ametys.runtime.plugins.admin.migration; 018 019import java.time.ZoneOffset; 020import java.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.List; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Set; 028import java.util.TreeMap; 029import java.util.TreeSet; 030import java.util.stream.Collectors; 031 032import org.apache.avalon.framework.component.Component; 033import org.apache.avalon.framework.configuration.ConfigurationException; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.commons.lang3.StringUtils; 038 039import org.ametys.core.migration.MigrationEngine; 040import org.ametys.core.migration.MigrationEngine.MigrationComponent; 041import org.ametys.core.migration.MigrationEngine.VersionList; 042import org.ametys.core.migration.MigrationEngine.Versions; 043import org.ametys.core.migration.MigrationEngine.VersionsContainer; 044import org.ametys.core.migration.MigrationException; 045import org.ametys.core.migration.MigrationExtensionPoint; 046import org.ametys.core.migration.action.ActionConfiguration; 047import org.ametys.core.migration.version.Version; 048import org.ametys.core.ui.Callable; 049import org.ametys.core.util.DateUtils; 050import org.ametys.core.util.I18nUtils; 051import org.ametys.runtime.i18n.I18nizableText; 052 053/** 054 * Component for retrieving info about all past and available automatic upgrades. 055 */ 056public class MigrationsStatus implements Serviceable, Component 057{ 058 private MigrationEngine _migrationEngine; 059 private MigrationExtensionPoint _migrationExtensionPoint; 060 private MigrationExtensionPoint _migrationInternalExtensionPoint; 061 private I18nUtils _i18nUtils; 062 063 public void service(ServiceManager manager) throws ServiceException 064 { 065 _migrationEngine = (MigrationEngine) manager.lookup(MigrationEngine.ROLE); 066 _migrationExtensionPoint = (MigrationExtensionPoint) manager.lookup(MigrationExtensionPoint.ROLE); 067 _migrationInternalExtensionPoint = (MigrationExtensionPoint) manager.lookup(MigrationExtensionPoint.ROLE + "/internal"); 068 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 069 } 070 071 /** 072 * Retrieves the status of automatic migrations 073 * @param clientParameters The client parameters such as the node to refresh 074 * @return a JSON view of all existing automatic migrations. 075 * @throws ConfigurationException if an error occurred reading the configuration 076 * @throws MigrationException if an error occurred retrieving the versions 077 */ 078 @Callable (rights = "Runtime_Rights_Admin_Access", context = "/admin") 079 public Map<String, Object> getMigrationsStatus(Map<String, Object> clientParameters) throws ConfigurationException, MigrationException 080 { 081 Map<String, List<Map<String, Object>>> migrationsByPlugins = new TreeMap<>(); 082 083 _migrationsByPlugins(true, migrationsByPlugins, _migrationInternalExtensionPoint); 084 _migrationsByPlugins(false, migrationsByPlugins, _migrationExtensionPoint); 085 086 List<Map<String, Object>> allNodes = migrationsByPlugins.entrySet().stream() 087 .map(entry -> _plugin2json(entry.getKey(), entry.getValue())) 088 .toList(); 089 090 List<Map<String, Object>> parentInfos = new ArrayList<>(); 091 return Map.of( 092 "children", _filter(allNodes, StringUtils.removeStart((String) clientParameters.get("path"), "/root"), parentInfos), 093 "parentInfos", parentInfos 094 ); 095 } 096 097 @SuppressWarnings("unchecked") 098 private List<Map<String, Object>> _filter(List<Map<String, Object>> allNodes, String path, List<Map<String, Object>> parentInfos) 099 { 100 if (StringUtils.isBlank(path)) 101 { 102 return allNodes; 103 } 104 else 105 { 106 int i = path.indexOf('/', 1); 107 i = i == -1 ? path.length() : i; 108 String thisNode = path.substring(1, i); 109 String subPath = path.substring(i); 110 111 for (Map<String, Object> map : allNodes) 112 { 113 if (thisNode.equals(map.get("component"))) 114 { 115 parentInfos.add(Map.of( 116 "failed", map.get("failed"), 117 "warning", map.get("warning") 118 )); 119 return _filter((List<Map<String, Object>>) map.get("children"), subPath, parentInfos); 120 } 121 } 122 123 throw new IllegalArgumentException("Cannot find component '" + thisNode + "'"); 124 } 125 } 126 127 private void _migrationsByPlugins(boolean isInternal, Map<String, List<Map<String, Object>>> migrationsByPlugins, MigrationExtensionPoint migrationExtensionPoint) 128 { 129 for (String extensionId : migrationExtensionPoint.getExtensionsIds()) 130 { 131 MigrationComponent component = migrationExtensionPoint.getExtension(extensionId); 132 List<Map<String, Object>> migrations = migrationsByPlugins.computeIfAbsent(component.pluginName(), k -> new ArrayList<>()); 133 134 try 135 { 136 // Processing stored versions 137 Versions versions = _migrationEngine.getVersions(component); 138 139 List<Map<String, Object>> children = _versions2json(isInternal, extensionId, versions, component.upgrades()); 140 141 migrations.add(_component2json(isInternal, extensionId, extensionId, children, component.versionHandlerType(), component.versionStorage().getId(), true, versions instanceof VersionList vl ? vl.id() : null)); 142 } 143 catch (MigrationException e) 144 { 145 migrations.add(_componentFailure2json(isInternal, extensionId, component.versionHandlerType(), component.versionStorage().getId(), e)); 146 } 147 } 148 } 149 150 private List<Map<String, Object>> _versions2json(boolean isInternal, String componentId, Versions versions, List<ActionConfiguration> existingUpgrades) 151 { 152 if (versions instanceof VersionsContainer versionsContainer) 153 { 154 return _versionContainer2json(isInternal, componentId, versionsContainer, existingUpgrades); 155 } 156 else if (versions instanceof VersionList versionList) 157 { 158 return _versionList2json(isInternal, componentId, versionList, existingUpgrades); 159 } 160 else 161 { 162 throw new IllegalArgumentException("Object " + versions + " is not supported"); 163 } 164 } 165 166 private List<Map<String, Object>> _versionContainer2json(boolean isInternal, String componentId, VersionsContainer versionsContainer, List<ActionConfiguration> existingUpgrades) 167 { 168 List<Map<String, Object>> children = new ArrayList<>(); 169 170 for (Entry<I18nizableText, Versions> entry : versionsContainer.entrySet()) 171 { 172 Versions versions = entry.getValue(); 173 List<Map<String, Object>> granChildren = _versions2json(isInternal, componentId, versions, existingUpgrades); 174 children.add(_component2json(isInternal, componentId, _i18nUtils.translate(entry.getKey()), granChildren, null, null, false, versions instanceof VersionList vl ? vl.id() : null)); 175 } 176 177 Collections.sort(children, new TreeComparator()); 178 179 return children; 180 } 181 182 private List<Map<String, Object>> _versionList2json(boolean isInternal, String componentId, VersionList versionList, List<ActionConfiguration> existingUpgrades) 183 { 184 List<Map<String, Object>> children = new ArrayList<>(); 185 186 Set<String> existingUpgradeNumbers = existingUpgrades.stream().map(ActionConfiguration::getVersionNumber).collect(Collectors.toSet()); 187 188 Set<String> done = new TreeSet<>(); 189 190 Version latestVersion = _migrationEngine.getLatestVersion(versionList.versions()); 191 192 for (Version version : versionList.versions()) 193 { 194 done.add(version.getVersionNumber()); 195 children.add(_version2json(isInternal, componentId, versionList.id(), version, latestVersion == version, "0".equals(version.getVersionNumber()) || existingUpgradeNumbers.contains(version.getVersionNumber()))); 196 } 197 198 if (_isFailedAction(versionList.id()) // There is a failed action 199 && _failedActionIsTheFutureCurrentVersion(existingUpgrades, done, latestVersion)) // That is the current pending action 200 { 201 done.add(StringUtils.defaultIfBlank(_migrationEngine.getFailedAction().targetVersionNumber(), _migrationEngine.getFailedAction().configuration().getVersionNumber())); 202 children.add(_failureVersion2json(isInternal, componentId, versionList.id())); 203 } 204 205 for (ActionConfiguration actionConfiguration : existingUpgrades) 206 { 207 if (!done.contains(actionConfiguration.getVersionNumber())) 208 { 209 children.add(_pendingVersion2json(isInternal, componentId, versionList.id(), actionConfiguration, _newest(done), _oldest(done))); 210 } 211 } 212 213 Collections.sort(children, new TreeComparator()); 214 Collections.reverse(children); 215 216 if (children.size() > 0 && !"0".equals(children.get(children.size() - 1).get("component")) 217 || children.size() == 0) 218 { 219 // Add the 0 version in the past if necessary 220 children.add(_pendingVersion2json(isInternal, componentId, versionList.id(), null, _newest(done), _oldest(done))); 221 } 222 223 return children; 224 } 225 226 private boolean _failedActionIsTheFutureCurrentVersion(List<ActionConfiguration> existingUpgrades, Set<String> done, Version latestVersion) 227 { 228 Set<String> pendingNumbers = new TreeSet<>(); 229 for (ActionConfiguration actionConfiguration : existingUpgrades) 230 { 231 String versionToHandle = actionConfiguration.getVersionNumber(); 232 if (!done.contains(versionToHandle) 233 && (latestVersion == null || latestVersion.getVersionNumber().compareTo(versionToHandle) < 0)) 234 { 235 pendingNumbers.add(versionToHandle); 236 } 237 } 238 239 return _migrationEngine.getFailedAction().currentVersion().getVersionNumber() != null && StringUtils.equals(_oldest(pendingNumbers), _migrationEngine.getFailedAction().configuration().getVersionNumber()) // Upgrade failed 240 || _migrationEngine.getFailedAction().currentVersion().getVersionNumber() == null && StringUtils.equals(StringUtils.defaultIfBlank(_newest(pendingNumbers), "0"), StringUtils.defaultIfBlank(_migrationEngine.getFailedAction().targetVersionNumber(), _migrationEngine.getFailedAction().configuration().getVersionNumber())); // Init failed 241 } 242 243 private Map<String, Object> _plugin2json(String pluginName, List<Map<String, Object>> children) 244 { 245 Collections.sort(children, new TreeComparator()); 246 247 return Map.of( 248 "component", pluginName, 249 "expanded", true, 250 "failed", _any(children, "failed"), 251 "warning", _any(children, "warning"), 252 "type", "plugin", 253 "children", children 254 ); 255 } 256 257 private Map<String, Object> _component2json(boolean isInternal, String componentId, String componentLabel, List<Map<String, Object>> children, String versionHandlerType, String versionStorageType, boolean rootComponent, String versionListId) 258 { 259 return Map.of( 260 "component", componentLabel, 261 "componentId", componentId, 262 "comment", rootComponent ? ("Type: " + versionHandlerType + (StringUtils.equals(versionHandlerType, versionStorageType) ? "" : "/" + versionStorageType)) : "", 263 "versionListId", StringUtils.defaultString(versionListId), 264 "failed", _any(children, "failed"), 265 "warning", _any(children, "warning"), 266 "type", rootComponent ? "component" : "container", 267 "internal", isInternal, 268 "children", children 269 ); 270 } 271 272 private Map<String, Object> _componentFailure2json(boolean isInternal, String componentId, String versionHandlerType, String versionStorageType, MigrationException ex) 273 { 274 return Map.of( 275 "component", componentId, 276 "comment", "Type: " + versionHandlerType + (StringUtils.equals(versionHandlerType, versionStorageType) ? "" : "/" + versionStorageType), 277 "errorComment", _migrationExceptionToCommentString(ex), 278 "failed", true, 279 "warning", false, 280 "internal", isInternal, 281 "type", "component" 282 ); 283 } 284 285 private Map<String, Object> _pendingVersion2json(boolean isInternal, String componentId, String versionListId, ActionConfiguration actionConfiguration, String currentStoredVersion, String oldestStoredVersion) 286 { 287 String versionNumber = actionConfiguration != null ? actionConfiguration.getVersionNumber() : "0"; 288 289 boolean past = currentStoredVersion != null && versionNumber.compareTo(currentStoredVersion) < 0; 290 boolean beforeInitPast = past && versionNumber.compareTo(oldestStoredVersion) < 0; 291 292 return Map.of( 293 "component", versionNumber, 294 "componentId", componentId, 295 "versionListId", versionListId, 296 "failed", false, 297 "warning", false, 298 "comment", actionConfiguration != null ? StringUtils.defaultString(actionConfiguration.getComment()) : "", 299 "instant", "", 300 "internal", isInternal, 301 "type", past ? (beforeInitPast ? "past-before" : "past-notdone") : "pending" 302 ); 303 } 304 305 private Map<String, Object> _failureVersion2json(boolean isInternal, String componentId, String versionListId) 306 { 307 return Map.of( 308 "component", StringUtils.defaultIfBlank(_migrationEngine.getFailedAction().targetVersionNumber(), _migrationEngine.getFailedAction().configuration().getVersionNumber()), 309 "componentId", componentId, 310 "versionListId", versionListId, 311 "failed", true, 312 "warning", false, 313 "comment", StringUtils.defaultString(_migrationEngine.getFailedAction().configuration().getComment()), 314 "errorComment", _migrationExceptionToCommentString(_migrationEngine.getFailedException()), 315 "instant", DateUtils.zonedDateTimeToString(ZonedDateTime.ofInstant(_migrationEngine.getFailedAction().currentVersion().getExecutionInstant(), ZoneOffset.UTC)), 316 "internal", isInternal, 317 "type", "error" 318 ); 319 } 320 321 private Map<String, Object> _version2json(boolean isInternal, String componentId, String versionListId, Version version, boolean current, boolean existing) 322 { 323 return Map.of( 324 "component", StringUtils.defaultString(version.getVersionNumber()), 325 "componentId", componentId, 326 "versionListId", versionListId, 327 "comment", StringUtils.defaultString(version.getComment()), 328 "errorComment", !existing ? _i18nUtils.translate(new I18nizableText("plugin.admin", "PLUGINS_ADMIN_TOOL_MIGRATIONS_COL_COMMENT_NONEXISTING")) : "", 329 "failed", false, 330 "warning", !existing, 331 "instant", version.getExecutionInstant() != null ? DateUtils.zonedDateTimeToString(ZonedDateTime.ofInstant(version.getExecutionInstant(), ZoneOffset.UTC)) : "", 332 "internal", isInternal, 333 "type", current ? "current" : "past-done" 334 ); 335 } 336 337 private String _newest(Set<String> s) 338 { 339 return s.size() > 0 ? s.stream().skip(s.size() - 1).findFirst().orElse(null) : null; 340 } 341 private String _oldest(Set<String> s) 342 { 343 return s.stream().findFirst().orElse(null); 344 } 345 346 private boolean _isFailedAction(String versionListId) 347 { 348 return _migrationEngine.getFailedAction() != null 349 && StringUtils.equals(versionListId, _migrationEngine.getFailedAction().versionListId()); 350 } 351 352 private String _migrationExceptionToCommentString(MigrationException ex) 353 { 354 return ex != null ? ex.getFailureMessage().replaceAll("<", "<").replaceAll("\n", "<br/>") : ""; 355 } 356 357 @SuppressWarnings({"cast", "unchecked"}) 358 private boolean _any(List<Map<String, Object>> children, String key) 359 { 360 for (Map<String, Object> child : children) 361 { 362 if (child.get(key) == Boolean.TRUE) 363 { 364 return true; 365 } 366 if (child.get("children") instanceof List granChildren 367 && _any((List<Map<String, Object>>) granChildren, key)) 368 { 369 return true; 370 } 371 } 372 373 return false; 374 } 375 376 private final class TreeComparator implements Comparator<Map> 377 { 378 public int compare(Map o1, Map o2) 379 { 380 return ((String) o1.get("component")).toLowerCase().compareTo(((String) o2.get("component")).toLowerCase()); 381 } 382 } 383}