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