001/* 002 * Copyright 2024 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.contentio.synchronize.impl; 017 018import java.util.Collection; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Map.Entry; 024import java.util.Set; 025import java.util.function.Function; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.configuration.Configuration; 029import org.apache.avalon.framework.configuration.ConfigurationException; 030import org.slf4j.Logger; 031 032import org.ametys.plugins.contentio.synchronize.AbstractSimpleSynchronizableContentsCollection; 033import org.ametys.runtime.i18n.I18nizableText; 034 035/** 036 * <p>Abstract for default and simplified implementation of SCC.</p> 037 * <p>Mandatory methods are:</p> 038 * <ul> 039 * <li>{@link #getIdField()} to define the content field which is the synchronization identifier.</li> 040 * <li>{@link #internalSearch(Map, int, int, List, Logger)} to search data. It supports pagination but on single search and global synchronization, pagination is set to 0 and {@link Integer#MAX_VALUE}. Also, on an import or synchronization (single search), the search parameters are filled with the {@link #getIdField()} as a parameter and its associated value. The values are organized by ID field then name and value, the value can be a {@link Collection}. May not return null.</li> 041 * </ul> 042 * <p>It may be useful to override the following methods too:</p> 043 * <ul> 044 * <li>{@link #getLocalAndExternalFields(Map)} to set the values from synchronization into remote values. These attributes name will have the synchronization toggle on edition.</li> 045 * <li>{@link #_getMapping(Map)} to modify the default mapping, default mapping consider the field name from results as the final attribute name. The mapping can receive data from several fields.</li> 046 * </ul> 047 * <p>You have to declare your now SCC model into a plugin.xml through the following extension:</p> 048 * <pre> 049 * <extension id="fr.ametys.myproject.scc.static" 050 * class="org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizableContentsCollectionModel" 051 * point="org.ametys.plugins.contentio.synchronize.SynchronizeContentsCollectionModelExtensionPoint"> 052 * <class name="org.ametys.plugins.contentio.synchronize.impl.StaticSynchronizableContentsCollection"/> 053 * <label i18n="false">Static SCC</label> 054 * <description i18n="false">Static SCC</description> 055 * </extension> 056 * </pre> 057 * <p>And it's also possible to declare a synchronization button on the content:</p> 058 * <pre> 059 * <extension id="fr.ametys.myproject.scc.static.SynchronizePerson" 060 * point="org.ametys.core.ui.RibbonControlsManager" 061 * class="org.ametys.plugins.contentio.synchronize.clientsideelement.SCCSmartContentClientSideElement"> 062 * <class name="Ametys.plugins.cms.content.controller.SmartContentController"> 063 * <action>Ametys.plugins.contentio.search.SynchronizeContentAction.act</action> 064 * 065 * <sccModelId>fr.ametys.myproject.scc.static</sccModelId> 066 * 067 * <label type="false">Synchroniser le contenu</label> 068 * <description type="false">Synchronisation du contenu</description> 069 * 070 * <field-label>Login</field-label> 071 * 072 * <selection-target-id>^content$</selection-target-id> 073 * <selection-target-parameter> 074 * <name>^types$</name> 075 * <value>^org.ametys.plugins.odf.Content.person$</value> 076 * </selection-target-parameter> 077 * <selection-enable-multiselection>false</selection-enable-multiselection> 078 * 079 * <icon-glyph>ametysicon-arrow123</icon-glyph> 080 * 081 * <selection-description-empty type="i18n">plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NOCONTENT</selection-description-empty> 082 * <selection-description-nomatch type="i18n">plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NOCONTENT</selection-description-nomatch> 083 * <selection-description-multiselectionforbidden type="i18n">plugin.cms:CONTENT_EDIT_DESCRIPTION_MANYCONTENT</selection-description-multiselectionforbidden> 084 * 085 * <allright-start-description type="i18n">plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_START</allright-start-description> 086 * <allright-end-description type="i18n">plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT_END</allright-end-description> 087 * <allright-content-description type="i18n">plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT</allright-content-description> 088 * <error-description type="i18n">plugin.cms:CONTENT_EDIT_DESCRIPTION_ERROR</error-description> 089 * 090 * <enabled-on-unlock-only>true</enabled-on-unlock-only> 091 * <locked-start-description type="i18n">plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_START</locked-start-description> 092 * <locked-end-description type="i18n">plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_END</locked-end-description> 093 * <locked-content-description type="i18n">plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_CONTENT</locked-content-description> 094 * </class> 095 * <scripts> 096 * <file plugin="cms">js/Ametys/plugins/cms/content/controller/SmartContentController.js</file> 097 * <file plugin="contentio">js/Ametys/plugins/contentio/search/SynchronizeContentAction.js</file> 098 * </scripts> 099 * <depends> 100 * <org.ametys.core.ui.UIToolsFactoriesManager>uitool-server-logs</org.ametys.core.ui.UIToolsFactoriesManager> 101 * </depends> 102 * </extension> 103 * </pre> 104 * <p>And a tool to search and import a single content:</p> 105 * <pre> 106 * <extension id="fr.ametys.myproject.scc.static.Import" 107 * point="org.ametys.core.ui.RibbonControlsManager" 108 * class="org.ametys.plugins.contentio.synchronize.clientsideelement.SCCClientSideElement"> 109 * <class name="Ametys.ribbon.element.ui.button.OpenToolButtonController"> 110 * <opentool-id>uitool-scc-import</opentool-id> 111 * 112 * <sccModelId>fr.ametys.myproject.scc.static</sccModelId> 113 * 114 * <label type="false">Importer un contenu</label> 115 * <description type="false">Import d'un contenu</description> 116 * 117 * <icon-glyph>ametysicon-body-people</icon-glyph> 118 * <icon-decorator>decorator-ametysicon-upload119</icon-decorator> 119 * <icon-decorator-type>action-create</icon-decorator-type> 120 * </class> 121 * <depends> 122 * <org.ametys.core.ui.UIToolsFactoriesManager>uitool-scc-import</org.ametys.core.ui.UIToolsFactoriesManager> 123 * </depends> 124 * </extension> 125 * </pre> 126 */ 127public abstract class AbstractDefaultSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection 128{ 129 public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters) 130 { 131 // By default, no synchronizable fields 132 return new HashSet<>(); 133 } 134 135 @Override 136 protected Map<String, Object> putIdParameter(String syncCode) 137 { 138 Map<String, Object> parameters = new HashMap<>(); 139 // Set the current content in search 140 parameters.put(getIdField(), syncCode); 141 return parameters; 142 } 143 144 @Override 145 protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger) 146 { 147 Map<String, Map<String, Object>> results = internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger); 148 return _sccHelper.organizeRemoteValuesByAttribute(results, _getMapping(results)); 149 } 150 151 @Override 152 public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger) 153 { 154 Map<String, Map<String, Object>> results = new HashMap<>(super.search(searchParameters, offset, limit, sort, logger)); 155 156 // Add SCC unique ID for search results 157 for (Entry<String, Map<String, Object>> result : results.entrySet()) 158 { 159 Map<String, Object> resultValues = new HashMap<>(result.getValue()); 160 if (!resultValues.containsKey(SCC_UNIQUE_ID)) 161 { 162 resultValues.put(SCC_UNIQUE_ID, result.getKey()); 163 results.put(result.getKey(), resultValues); 164 } 165 } 166 167 return results; 168 } 169 170 @Override 171 protected void configureDataSource(Configuration configuration) throws ConfigurationException 172 { 173 // Do nothing 174 } 175 176 @Override 177 protected void configureSearchModel() 178 { 179 _searchModelConfiguration.addColumn(SCC_UNIQUE_ID, new I18nizableText(getIdField()), false); 180 } 181 182 /** 183 * Get the mapping of the current SCC based on results. Default implementation is key -> List.of(key). 184 * @param results The results 185 * @return the mapping based on results (if needed) 186 */ 187 protected Map<String, List<String>> _getMapping(Map<String, Map<String, Object>> results) 188 { 189 // Get all columns names and organize it as mapping 190 // Default implementation is key -> List.of(key) and add the synchronization code mapping too if not in the results list 191 Map<String, List<String>> mapping = results.values() 192 .stream() 193 .map(Map::keySet) 194 .flatMap(Set::stream) 195 .distinct() 196 .collect(Collectors.toMap(Function.identity(), key -> List.of(key))); 197 mapping.putIfAbsent(getIdField(), List.of(getIdField())); 198 return mapping; 199 } 200}