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.cms.content.version;
017
018import java.io.IOException;
019import java.net.URISyntaxException;
020import java.nio.charset.StandardCharsets;
021import java.util.Collection;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.cocoon.ProcessingException;
031import org.apache.commons.io.IOUtils;
032import org.apache.excalibur.source.Source;
033import org.apache.excalibur.source.SourceResolver;
034import org.apache.http.client.utils.URIBuilder;
035
036import org.ametys.cms.content.compare.ContentComparatorChange;
037import org.ametys.cms.content.compare.ContentComparatorResult;
038import org.ametys.cms.contenttype.ContentTypesHelper;
039import org.ametys.cms.repository.Content;
040import org.ametys.cms.rights.ContentRightAssignmentContext;
041import org.ametys.core.ui.Callable;
042import org.ametys.core.ui.ClientSideElement;
043import org.ametys.core.ui.StaticClientSideElement;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
046import org.ametys.runtime.model.View;
047import org.ametys.runtime.model.ViewHelper;
048
049/**
050 * {@link ClientSideElement} for the tool for comparing a content between a base version and a target version. 
051 */
052public class CompareContentVersionToolClientSideElement extends StaticClientSideElement
053{
054    private CompareVersionHelper _compareVersionHelper;
055    private SourceResolver _sourceResolver;
056    private ContentTypesHelper _cTypesHelper;
057    private ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
058
059    @Override
060    public void service(ServiceManager smanager) throws ServiceException
061    {
062        super.service(smanager);
063        _compareVersionHelper = (CompareVersionHelper) smanager.lookup(CompareVersionHelper.ROLE);
064        _sourceResolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE);
065        _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE);
066        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) smanager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
067    }
068    
069    /**
070     * Gets the JSON information about the changes for the given content id between the base and target provided versions.
071     * @param contentId The {@link Content} id
072     * @param viewName The view to use
073     * @param showAllData true to display all data, false to display only data with diff
074     * @param targetVersion The content version to be compared
075     * @param baseVersion The base content version
076     * @return The information
077     * @throws IOException If an I/O exception occurred during the comparison between the two versions, or when getting content values
078     * @throws ProcessingException If an exception occurred when converting the "change" view
079     */
080    @Callable(rights = "CMS_Rights_Content_History", paramIndex = 0, rightContext = ContentRightAssignmentContext.ID)
081    public Map<String, Object> getDiffValues(String contentId, String viewName, boolean showAllData, String targetVersion, String baseVersion) throws IOException, ProcessingException
082    {
083        ContentVersionComparator comparator = new ContentVersionComparator(contentId, viewName, showAllData, targetVersion, baseVersion);
084        comparator.compare();
085        return comparator.resultToJson();
086    }
087    
088    private class ContentVersionComparator
089    {
090        private final String _contentId;
091        private final String _baseVersion;
092        private final String _targetVersion;
093        private Content _targetContent;
094        private ContentComparatorResult _comparatorResult;
095        private CompareView _resultCompareView;
096        private String _viewName;
097        private boolean _showAllData;
098        
099        ContentVersionComparator(String contentId, String viewName, boolean showAllData, String version, String baseVersion)
100        {
101            _contentId = contentId;
102            _viewName = viewName;
103            _showAllData = showAllData;
104            _baseVersion = baseVersion;
105            _targetVersion = version;
106        }
107        
108        void compare() throws IOException
109        {
110            _targetContent = _compareVersionHelper.getContentVersion(_contentId, _targetVersion);
111            
112            _comparatorResult = _compareVersionHelper.compareVersions(_contentId, _targetVersion, _baseVersion);
113            
114            View wrappedEditionView = ViewHelper.getTruncatedView(_retrieveEditionView());
115            Set<String> externalizableDataPaths = _externalizableDataProviderEP.getExternalizableDataPaths(_targetContent);
116            _resultCompareView = new CompareView(wrappedEditionView, externalizableDataPaths, _showAllData, _comparatorResult);
117        }
118        
119        Map<String, Object> resultToJson() throws IOException, ProcessingException
120        {
121            return Map.of(
122                    "view", _resultCompareViewToJson(), 
123                    "changedAttributeDataPaths", _changedAttributeDataPaths(),
124                    // FIXME REPOSITORY-454 Use "baseValues", _contentToJson(_baseContent), 
125                    "baseValues", _saxedContent(_baseVersion), 
126                    // FIXME REPOSITORY-454 Use "values", _contentToJson(_targetContent));
127                    "values", _saxedContent(_targetVersion));
128        }
129        
130        private Map<String, Object> _resultCompareViewToJson() throws ProcessingException
131        {
132            return _resultCompareView.toJSON();
133            
134        }
135        
136        private Collection<String> _changedAttributeDataPaths() throws AmetysRepositoryException
137        {
138            List<ContentComparatorChange> contentComparatorChanges = _comparatorResult.getChanges();
139            return _compareVersionHelper.filterChanges(contentComparatorChanges)
140                    .map(ContentComparatorChange::getAttributeDataPath)
141                    .collect(Collectors.toList());
142        }
143        
144//        private Map<String, Object> _contentToJson(Content content)
145//        {
146//            return content.toJson(_changesView);
147//        }
148        
149        private String _saxedContent(String contentVersion) throws IOException
150        {
151            // FIXME REPOSITORY-454 use Content#toJson instead, to manipulate JSON and to be able to provide the view through a simple API call, instead of calling '/_content.xml' URI
152            // * when done, content#toJson will be available and the line for _targetContent can be uncommented, as well as declaration and calls of _contentToJson
153            // * when done, _changesView can be passed to this method
154            String uri;
155            try
156            {
157                // cocoon://_content.xml?contentId=...
158                uri = new URIBuilder()
159                        .setScheme("cocoon")
160                        .setHost("_content.xml")
161                        .addParameter("contentId", _contentId)
162                        .addParameter("contentVersion", contentVersion)
163                        .addParameter("viewName", _viewName)
164                        .addParameter("fallbackViewName", "main")
165                        .addParameter("isEdition", "true")
166                        .build()
167                        .toString();
168            }
169            catch (URISyntaxException e)
170            {
171                throw new IOException(e);
172            }
173            
174            Source saxedContent = _sourceResolver.resolveURI(uri);
175            return IOUtils.toString(saxedContent.getInputStream(), StandardCharsets.UTF_8);
176        }
177        
178        private View _retrieveEditionView()
179        {
180            String[] contentTypeIds = _targetContent.getTypes();
181            String[] mixinIds = _targetContent.getMixinTypes();
182            View view = _cTypesHelper.getView(_viewName, contentTypeIds, mixinIds);
183            if (view == null)
184            {
185                view = _cTypesHelper.getView("main", contentTypeIds, mixinIds);
186            }
187            return Objects.requireNonNull(view);
188        }
189    }
190}