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;allright-start-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_START&lt;/allright-start-description&gt;
086 *          &lt;allright-end-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT_END&lt;/allright-end-description&gt;
087 *          &lt;allright-content-description type="i18n"&gt;plugin.contentio:PLUGINS_CONTENTIO_BUTTON_SYNCHRONIZE_CONTENT&lt;/allright-content-description&gt;
088 *          &lt;error-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_ERROR&lt;/error-description&gt;
089 * 
090 *          &lt;enabled-on-unlock-only&gt;true&lt;/enabled-on-unlock-only&gt;
091 *          &lt;locked-start-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_START&lt;/locked-start-description&gt;
092 *          &lt;locked-end-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_END&lt;/locked-end-description&gt;
093 *          &lt;locked-content-description type="i18n"&gt;plugin.cms:CONTENT_EDIT_DESCRIPTION_LOCKED_CONTENT&lt;/locked-content-description&gt;
094 *      &lt;/class&gt;
095 *      &lt;scripts&gt;
096 *          &lt;file plugin="cms"&gt;js/Ametys/plugins/cms/content/controller/SmartContentController.js&lt;/file&gt;
097 *          &lt;file plugin="contentio"&gt;js/Ametys/plugins/contentio/search/SynchronizeContentAction.js&lt;/file&gt;
098 *      &lt;/scripts&gt;
099 *      &lt;depends&gt;
100 *          &lt;org.ametys.core.ui.UIToolsFactoriesManager&gt;uitool-server-logs&lt;/org.ametys.core.ui.UIToolsFactoriesManager&gt;
101 *      &lt;/depends&gt;
102 *  &lt;/extension&gt;
103 * </pre>
104 * <p>And a tool to search and import a single content:</p>
105 * <pre>
106 *  &lt;extension id="fr.ametys.myproject.scc.static.Import"
107 *                point="org.ametys.core.ui.RibbonControlsManager"
108 *                class="org.ametys.plugins.contentio.synchronize.clientsideelement.SCCClientSideElement"&gt;
109 *      &lt;class name="Ametys.ribbon.element.ui.button.OpenToolButtonController"&gt;
110 *          &lt;opentool-id&gt;uitool-scc-import&lt;/opentool-id&gt;
111 * 
112 *          &lt;sccModelId&gt;fr.ametys.myproject.scc.static&lt;/sccModelId&gt;
113 * 
114 *          &lt;label type="false"&gt;Importer un contenu&lt;/label&gt;
115 *          &lt;description type="false"&gt;Import d'un contenu&lt;/description&gt;
116 * 
117 *          &lt;icon-glyph&gt;ametysicon-body-people&lt;/icon-glyph&gt;
118 *          &lt;icon-decorator&gt;decorator-ametysicon-upload119&lt;/icon-decorator&gt;
119 *          &lt;icon-decorator-type&gt;action-create&lt;/icon-decorator-type&gt;
120 *      &lt;/class&gt;
121 *      &lt;depends&gt;
122 *          &lt;org.ametys.core.ui.UIToolsFactoriesManager&gt;uitool-scc-import&lt;/org.ametys.core.ui.UIToolsFactoriesManager&gt;
123 *      &lt;/depends&gt;
124 *  &lt;/extension&gt;
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}