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