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}