001/* 002 * Copyright 2020 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.repository.migration.jcr.data; 017 018import java.time.ZoneId; 019import java.time.ZonedDateTime; 020import java.util.List; 021import java.util.Optional; 022import java.util.stream.Collectors; 023 024import org.apache.avalon.framework.configuration.Configuration; 025import org.apache.avalon.framework.configuration.ConfigurationException; 026import org.apache.avalon.framework.service.ServiceException; 027import org.apache.avalon.framework.service.ServiceManager; 028import org.apache.avalon.framework.service.Serviceable; 029 030import org.ametys.core.migration.MigrationException; 031import org.ametys.core.migration.MigrationExtensionPoint; 032import org.ametys.core.migration.configuration.VersionConfiguration; 033import org.ametys.core.migration.storage.VersionStorage; 034import org.ametys.core.migration.version.Version; 035import org.ametys.core.util.DateUtils; 036import org.ametys.plugins.repository.AmetysObjectIterable; 037import org.ametys.plugins.repository.AmetysObjectResolver; 038import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 039import org.ametys.plugins.repository.migration.jcr.data.configuration.impl.JcrVersionConfiguration; 040import org.ametys.plugins.repository.migration.jcr.data.repository.VersionAmetysObject; 041import org.ametys.plugins.repository.migration.jcr.data.repository.VersionComponentAmetysObject; 042import org.ametys.plugins.repository.migration.jcr.data.repository.VersionComponentFactory; 043import org.ametys.plugins.repository.migration.jcr.data.repository.VersionFactory; 044import org.ametys.plugins.repository.migration.jcr.data.repository.VersionsAmetysObject; 045import org.ametys.plugins.repository.query.QueryHelper; 046import org.ametys.plugins.repository.query.SortCriteria; 047import org.ametys.plugins.repository.query.expression.Expression; 048import org.ametys.plugins.repository.query.expression.Expression.Operator; 049import org.ametys.plugins.repository.query.expression.StringExpression; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051 052/** 053 * JCR implementation of {@link VersionStorage}, to manage the list of versions in a repository 054 */ 055public class JcrDataVersionStorage extends AbstractLogEnabled implements VersionStorage, Serviceable 056{ 057 /** The Ametys object resolver */ 058 protected AmetysObjectResolver _resolver; 059 060 /** The migration extension point */ 061 protected MigrationExtensionPoint _migrationEP; 062 063 /** The versions root helper */ 064 protected VersionsRootHelper _versionsRootHelper; 065 066 @Override 067 public void service(ServiceManager manager) throws ServiceException 068 { 069 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 070 _migrationEP = (MigrationExtensionPoint) manager.lookup(MigrationExtensionPoint.ROLE); 071 _versionsRootHelper = (VersionsRootHelper) manager.lookup(VersionsRootHelper.ROLE); 072 } 073 074 @Override 075 public Version getCurrentVersion(String componentIdentifier, VersionConfiguration configuration, String versionHandlerId) throws MigrationException 076 { 077 getLogger().debug("Get current version for component id: " + componentIdentifier); 078 if (configuration == null || !(configuration instanceof JcrVersionConfiguration)) 079 { 080 throw new MigrationException("Configuration object should be a JcrVersionConfiguration"); 081 } 082 083 // Case 1 - The versions node doesn't exist (repository from an older version than first migration implementations) 084 if (!_versionsRootHelper.hasRootObject()) 085 { 086 throw new MigrationException("Your existing repository does not support automatic migrations (ametys:versions does not exist). Please follow the migration guide."); 087 } 088 089 Optional<Version> currentVersion = _getCurrentVersion(versionHandlerId, componentIdentifier); 090 091 // Case 2 - The component is already versionned 092 if (currentVersion.isPresent()) 093 { 094 getLogger().debug("End get current version for component id: " + componentIdentifier + ", case 'already versionned'"); 095 return currentVersion.get(); 096 } 097 098 // Case 3 - The repository is a new one (first start of the application), or new repository but no migration finished yet 099 if (!_versionsRootHelper.hasKnownPlugins()) 100 { 101 getLogger().debug("End get current version for component id: " + componentIdentifier + ", case 'new repository'"); 102 return new JcrDataVersion(versionHandlerId, componentIdentifier); 103 } 104 105 // Case 4 - The plugin of the component is not referenced into the already loaded plugins (the plugin has been newly added) 106 String componentPluginName = _migrationEP.getExtension(componentIdentifier).getPluginName(); 107 if (!_versionsRootHelper.isAKnownPlugin(componentPluginName)) 108 { 109 getLogger().debug("End get current version for component id: " + componentIdentifier + ", case 'new plugin'"); 110 return new JcrDataVersion(versionHandlerId, componentIdentifier); 111 } 112 113 // Case 5 - We can't determine the last state of the component (all other cases) 114 Boolean failOnOldData = ((JcrVersionConfiguration) configuration).failOnOldData(); 115 if (failOnOldData) 116 { 117 throw new MigrationException("Your existing data for the component '" + componentIdentifier + "' does not support automatic migrations. Please follow the migration guide."); 118 } 119 120 // Case 6 - The component has always been versionned (so if you don't have the version for it, it's a new component) 121 // Version 0 by default, all updates are applied 122 Version version = new JcrDataVersion(versionHandlerId, componentIdentifier); 123 version.setVersionNumber("0"); 124 getLogger().debug("End get current version for component id: " + componentIdentifier + ", case 'already versionned, but without version stored'"); 125 return version; 126 } 127 128 @Override 129 public List<Version> getAllVersions(String componentIdentifier, VersionConfiguration configuration, String versionHandlerId) throws MigrationException 130 { 131 getLogger().debug("getAllVersions for component: {}", componentIdentifier); 132 // Build expression 133 Expression expression = new StringExpression(VersionAmetysObject.COMPONENT_ID, Operator.EQ, componentIdentifier); 134 135 String query = QueryHelper.getXPathQuery(null, VersionFactory.VERSION_NODETYPE, expression); 136 return _resolver.<VersionAmetysObject>query(query) 137 .stream() 138 .map(ao -> new JcrDataVersion(versionHandlerId, ao)) 139 .collect(Collectors.toList()); 140 } 141 142 @Override 143 public void addVersion(Version version) throws MigrationException 144 { 145 getLogger().debug("Add version for: {}", version.toString()); 146 if (!(version instanceof JcrDataVersion)) 147 { 148 throw new MigrationException("Impossible to create a JCR data version in a non JCR data version: " + version.toString()); 149 } 150 151 VersionComponentAmetysObject component = _getComponentObject(version.getComponentId(), true); 152 try 153 { 154 VersionAmetysObject versionAO = component.createChild("v-" + version.getVersionNumber(), VersionFactory.VERSION_NODETYPE); 155 versionAO.setComponentId(version.getComponentId()); 156 versionAO.setVersionNumber(version.getVersionNumber()); 157 versionAO.setExecutionDate(DateUtils.asCalendar(ZonedDateTime.ofInstant(version.getExecutionInstant(), ZoneId.of("UTC")))); 158 versionAO.setComment(version.getComment()); 159 component.saveChanges(); 160 ((JcrDataVersion) version).setAmetysObject(versionAO); 161 } 162 catch (RepositoryIntegrityViolationException e) 163 { 164 throw new MigrationException("JCR data version is instable for: " + version.toString(), e); 165 } 166 getLogger().debug("End add version for: {}", version.toString()); 167 } 168 169 @Override 170 public void removeAllVersions(String componentIdentifier, VersionConfiguration configuration) throws MigrationException 171 { 172 getLogger().debug("Start remove all version for component: {}", componentIdentifier); 173 VersionComponentAmetysObject component = _getComponentObject(componentIdentifier, false); 174 if (component != null) 175 { 176 VersionsAmetysObject parent = component.getParent(); 177 component.remove(); 178 parent.saveChanges(); 179 } 180 } 181 182 private Optional<Version> _getCurrentVersion(String versionHandlerId, String componentId) 183 { 184 getLogger().debug("Start _getCurrentVersion for component: {}", componentId); 185 // Build expression 186 Expression expression = new StringExpression(VersionAmetysObject.COMPONENT_ID, Operator.EQ, componentId); 187 188 // Build criteria 189 SortCriteria sortCriteria = new SortCriteria(); 190 sortCriteria.addCriterion(VersionAmetysObject.VERSION_NUMBER, false, true); 191 192 String query = QueryHelper.getXPathQuery(null, VersionFactory.VERSION_NODETYPE, expression, sortCriteria); 193 AmetysObjectIterable<VersionAmetysObject> versionsAO = _resolver.query(query); 194 195 getLogger().debug("End _getCurrentVersion for component: {}", componentId); 196 return versionsAO.stream() 197 .findFirst() 198 .map(ao -> new JcrDataVersion(versionHandlerId, ao)); 199 } 200 201 private VersionComponentAmetysObject _getComponentObject(String componentId, boolean create) 202 { 203 getLogger().debug("Start _getComponentObject for component: {}", componentId); 204 // Build expression 205 Expression expression = new StringExpression(VersionAmetysObject.COMPONENT_ID, Operator.EQ, componentId); 206 207 String query = QueryHelper.getXPathQuery(null, VersionComponentFactory.VERSION_COMPONENT_NODETYPE, expression); 208 AmetysObjectIterable<VersionComponentAmetysObject> versionsAO = _resolver.query(query); 209 getLogger().debug("End _getComponentObject for component: {}", componentId); 210 return versionsAO.stream() 211 .findFirst() 212 .orElseGet(() -> create ? _createComponentObject(componentId) : null); 213 } 214 215 private VersionComponentAmetysObject _createComponentObject(String componentId) 216 { 217 getLogger().debug("Start _createComponentObject for component: {}", componentId); 218 VersionsAmetysObject versions = _versionsRootHelper.getRootObject(); 219 220 // Get the child name 221 String componentName = "c-" 222 + componentId 223 .toLowerCase() 224 .replaceAll("[^a-z0-9]", " ") 225 .trim() 226 .replaceAll("[ ]{2,}", " ") 227 .replace(" ", "-"); 228 229 String childName = componentName; 230 int errorCount = 0; 231 while (versions.hasChild(childName)) 232 { 233 errorCount++; 234 childName = componentName + errorCount; 235 } 236 237 // Create version component node 238 VersionComponentAmetysObject versionComponent = versions.createChild(childName, VersionComponentFactory.VERSION_COMPONENT_NODETYPE); 239 versionComponent.setComponentId(componentId); 240 versions.saveChanges(); 241 getLogger().debug("End _createComponentObject for component: {}", componentId); 242 243 return versionComponent; 244 } 245 246 public VersionConfiguration getConfiguration(String componentId, Configuration versionConfiguration) throws ConfigurationException 247 { 248 getLogger().debug("Start get configuration for component: {}", componentId); 249 250 if (!"jcr-data".equals(versionConfiguration.getAttribute("type"))) 251 { 252 throw new ConfigurationException("The configuration node must be of type 'jcr-data'.", versionConfiguration); 253 } 254 255 return new JcrVersionConfiguration(Boolean.valueOf(versionConfiguration.getAttribute("failOnOldData", "false"))); 256 } 257}