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}