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 *  &lt;extension id="fr.ametys.myproject.scc.static"
050 *               class="org.ametys.plugins.contentio.synchronize.impl.DefaultSynchronizableContentsCollectionModel"
051 *               point="org.ametys.plugins.contentio.synchronize.SynchronizeContentsCollectionModelExtensionPoint"&gt;
052 *      &lt;class name="org.ametys.plugins.contentio.synchronize.impl.StaticSynchronizableContentsCollection"/&gt;
053 *          &lt;label i18n="false"&gt;Static SCC&lt;/label&gt;
054 *          &lt;description i18n="false"&gt;Static SCC&lt;/description&gt;
055 *  &lt;/extension&gt;
056 * </pre>
057 * <p>And it's also possible to declare a synchronization button on the content:</p>
058 * <pre>
059 *  &lt;extension id="fr.ametys.myproject.scc.static.SynchronizePerson"
060 *                point="org.ametys.core.ui.RibbonControlsManager"
061 *                class="org.ametys.plugins.contentio.synchronize.clientsideelement.SCCSmartContentClientSideElement"&gt;
062 *      &lt;class name="Ametys.plugins.cms.content.controller.SmartContentController"&gt;
063 *          &lt;action&gt;Ametys.plugins.contentio.search.SynchronizeContentAction.act&lt;/action&gt;
064 * 
065 *          &lt;sccModelId&gt;fr.ametys.myproject.scc.static&lt;/sccModelId&gt;
066 * 
067 *          &lt;label type="false"&gt;Synchroniser le contenu&lt;/label&gt;
068 *          &lt;description type="false"&gt;Synchronisation du contenu&lt;/description&gt;
069 * 
070 *          &lt;field-label&gt;Login&lt;/field-label&gt;
071 * 
072 *          &lt;selection-target-id&gt;^content$&lt;/selection-target-id&gt;
073 *          &lt;selection-target-parameter&gt;
074 *              &lt;name&gt;^types$&lt;/name&gt;
075 *              &lt;value&gt;^org.ametys.plugins.odf.Content.person$&lt;/value&gt;
076 *          &lt;/selection-target-parameter&gt;
077 *          &lt;selection-enable-multiselection&gt;false&lt;/selection-enable-multiselection&gt;
078 * 
079 *          &lt;icon-glyph&gt;ametysicon-arrow123&lt;/icon-glyph&gt;
080 * 
081 *          &lt;selection-description-empty type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NOCONTENT&lt;/selection-description-empty&gt;
082 *          &lt;selection-description-nomatch type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NOCONTENT&lt;/selection-description-nomatch&gt;
083 *          &lt;selection-description-multiselectionforbidden type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_MANYCONTENT&lt;/selection-description-multiselectionforbidden&gt;
084 *          
085 *          &lt;enabled-on-right-only&gt;true&lt;/enabled-on-right-only&gt;
086 *          &lt;rights&gt;RIGHT_ID&lt;/rights&gt;
087 *          &lt;noright-start-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NORIGHT_START&lt;/noright-start-description&gt;
088 *          &lt;noright-end-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NORIGHT_END&lt;/noright-end-description&gt;
089 *          &lt;noright-content-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_NORIGHT_CONTENT&lt;/noright-content-description&gt;
090 * 
091 *          &lt;allright-start-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_START&lt;/allright-start-description&gt;
092 *          &lt;allright-end-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT_END&lt;/allright-end-description&gt;
093 *          &lt;allright-content-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT&lt;/allright-content-description&gt;
094 *          &lt;error-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_ERROR&lt;/error-description&gt;
095 * 
096 *          &lt;enabled-on-unlock-only&gt;true&lt;/enabled-on-unlock-only&gt;
097 *          &lt;locked-start-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_START&lt;/locked-start-description&gt;
098 *          &lt;locked-end-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_END&lt;/locked-end-description&gt;
099 *          &lt;locked-content-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_CONTENT&lt;/locked-content-description&gt;
100 *      &lt;/class&gt;
101 *      &lt;scripts&gt;
102 *          &lt;file plugin="cms"&gt;js/Ametys/plugins/cms/content/controller/SmartContentController.js&lt;/file&gt;
103 *          &lt;file plugin="contentio"&gt;js/Ametys/plugins/contentio/search/SynchronizeContentAction.js&lt;/file&gt;
104 *      &lt;/scripts&gt;
105 *      &lt;depends&gt;
106 *          &lt;org.ametys.core.ui.UIToolsFactoriesManager&gt;uitool-server-logs&lt;/org.ametys.core.ui.UIToolsFactoriesManager&gt;
107 *      &lt;/depends&gt;
108 *  &lt;/extension&gt;
109 * </pre>
110 * <p>And a tool to search and import a single content:</p>
111 * <pre>
112 *  &lt;extension id="fr.ametys.myproject.scc.static.Import"
113 *                point="org.ametys.core.ui.RibbonControlsManager"
114 *                class="org.ametys.plugins.contentio.synchronize.clientsideelement.SCCClientSideElement"&gt;
115 *      &lt;class name="Ametys.ribbon.element.ui.button.OpenToolButtonController"&gt;
116 *          &lt;opentool-id&gt;uitool-scc-import&lt;/opentool-id&gt;
117 *          &lt;opentool-params&gt;
118 *              &lt;controllerId&gt;fr.ametys.myproject.scc.static.Import&lt;/controllerId&gt;
119 *          &lt;/opentool-params&gt;
120 * 
121 *          &lt;sccModelId&gt;fr.ametys.myproject.scc.static&lt;/sccModelId&gt;
122 * 
123 *          &lt;label type="false"&gt;Importer un contenu&lt;/label&gt;
124 *          &lt;description type="false"&gt;Import d'un contenu&lt;/description&gt;
125 * 
126 *          &lt;icon-glyph&gt;ametysicon-body-people&lt;/icon-glyph&gt;
127 *          &lt;icon-decorator&gt;decorator-ametysicon-upload119&lt;/icon-decorator&gt;
128 *          &lt;icon-decorator-type&gt;action-create&lt;/icon-decorator-type&gt;
129 *      &lt;/class&gt;
130 *      &lt;depends&gt;
131 *          &lt;org.ametys.core.ui.UIToolsFactoriesManager&gt;uitool-scc-import&lt;/org.ametys.core.ui.UIToolsFactoriesManager&gt;
132 *      &lt;/depends&gt;
133 *  &lt;/extension&gt;
134 * </pre>
135 * <p>The feature containing this tool must depend on contentio/org.ametys.plugins.contentio.scc.search.tool feature</p>
136 */
137public abstract class AbstractDefaultSynchronizableContentsCollection extends AbstractSimpleSynchronizableContentsCollection
138{
139    public Set<String> getLocalAndExternalFields(Map<String, Object> additionalParameters)
140    {
141        // By default, no synchronizable fields
142        return new HashSet<>();
143    }
144
145    @Override
146    protected Map<String, Object> putIdParameter(String syncCode)
147    {
148        Map<String, Object> parameters = new HashMap<>();
149        // Set the current content in search
150        parameters.put(getIdField(), syncCode);
151        return parameters;
152    }
153    
154    @Override
155    protected Map<String, Map<String, List<Object>>> getRemoteValues(Map<String, Object> searchParameters, Logger logger)
156    {
157        Map<String, Map<String, Object>> results = internalSearch(searchParameters, 0, Integer.MAX_VALUE, null, logger);
158        return _sccHelper.organizeRemoteValuesByAttribute(results, _getMapping(results));
159    }
160    
161    @Override
162    public Map<String, Map<String, Object>> search(Map<String, Object> searchParameters, int offset, int limit, List<Object> sort, Logger logger)
163    {
164        Map<String, Map<String, Object>> results = new HashMap<>(super.search(searchParameters, offset, limit, sort, logger));
165        
166        // Add SCC unique ID for search results
167        for (Entry<String, Map<String, Object>> result : results.entrySet())
168        {
169            Map<String, Object> resultValues = new HashMap<>(result.getValue());
170            if (!resultValues.containsKey(SCC_UNIQUE_ID))
171            {
172                resultValues.put(SCC_UNIQUE_ID, result.getKey());
173                results.put(result.getKey(), resultValues);
174            }
175        }
176        
177        return results;
178    }
179    
180    @Override
181    protected void configureDataSource(Configuration configuration) throws ConfigurationException
182    {
183        // Do nothing
184    }
185
186    @Override
187    protected void configureSearchModel()
188    {
189        _searchModelConfiguration.addColumn(SCC_UNIQUE_ID, new I18nizableText(getIdField()), false);
190    }
191    
192    /**
193     * Get the mapping of the current SCC based on results. Default implementation is key -> List.of(key).
194     * @param results The results
195     * @return the mapping based on results (if needed)
196     */
197    protected Map<String, List<String>> _getMapping(Map<String, Map<String, Object>> results)
198    {
199        // Get all columns names and organize it as mapping
200        // Default implementation is key -> List.of(key) and add the synchronization code mapping too if not in the results list
201        Map<String, List<String>> mapping = results.values()
202                .stream()
203                .map(Map::keySet)
204                .flatMap(Set::stream)
205                .distinct()
206                .collect(Collectors.toMap(Function.identity(), key -> List.of(key)));
207        mapping.putIfAbsent(getIdField(), List.of(getIdField()));
208        return mapping;
209    }
210}