001/*
002 *  Copyright 2018 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.web.repository.page;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.ProcessingException;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.commons.lang.StringUtils;
036
037import org.ametys.core.observation.Event;
038import org.ametys.core.observation.ObservationManager;
039import org.ametys.core.ui.Callable;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.plugins.repository.AmetysObjectResolver;
042import org.ametys.plugins.repository.UnknownAmetysObjectException;
043import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
044import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
045import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
046import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
047import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
048import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeaterEntry;
049import org.ametys.plugins.repository.model.RepeaterDefinition;
050import org.ametys.runtime.i18n.I18nizableText;
051import org.ametys.runtime.model.DefinitionAndValue;
052import org.ametys.runtime.model.ElementDefinition;
053import org.ametys.runtime.model.ModelHelper;
054import org.ametys.runtime.model.ModelItem;
055import org.ametys.runtime.model.ModelItemContainer;
056import org.ametys.runtime.model.View;
057import org.ametys.runtime.model.type.ElementType;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059import org.ametys.web.ObservationConstants;
060import org.ametys.web.WebConstants;
061import org.ametys.web.repository.page.Page.PageType;
062import org.ametys.web.repository.page.ZoneItem.ZoneType;
063import org.ametys.web.service.Service;
064import org.ametys.web.service.ServiceExtensionPoint;
065
066/**
067 * Class containing callables to retrieve and configure service parameters
068 */
069public class ZoneItemManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
070{
071    /** Avalon Role */
072    public static final String ROLE = ZoneItemManager.class.getName();
073    
074    /** Constant for untouched binary metadata. */
075    protected static final String __SERVICE_PARAM_UNTOUCHED_BINARY = "untouched";
076    
077    private ServiceExtensionPoint _serviceExtensionPoint;
078    private AmetysObjectResolver _resolver;
079    private ObservationManager _observationManager;
080    private CurrentUserProvider _currentUserProvider;
081    private Context _context;
082
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        _serviceExtensionPoint = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
086        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
087        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
088        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
089    }
090    
091    public void contextualize(Context context) throws ContextException
092    {
093        _context = context;
094    }
095    
096    /**
097     * Retrieves the parameter definitions of the given service
098     * @param serviceId Identifier of the service
099     * @param pageId the page id
100     * @param zoneItemId the zone item id
101     * @param zoneName the zone name
102     * @return the parameter definitions
103     * @throws ProcessingException if an error occurs
104     */
105    @Callable
106    public Map<String, Object> getServiceParameterDefinitions(String serviceId, String pageId, String zoneItemId, String zoneName) throws ProcessingException
107    {
108        _setRequestAttribute(serviceId, pageId, zoneItemId, zoneName);
109        
110        Map<String, Object> response = new HashMap<>();
111        
112        Service service = _serviceExtensionPoint.getExtension(serviceId);
113        
114        response.put("id", serviceId);
115        response.put("url", service.getURL());
116        response.put("label", service.getLabel());
117        response.put("height", service.getCreationBoxHeight());
118        response.put("width", service.getCreationBoxWidth());
119        
120        View serviceView = service.getView();
121        response.put("parameters", serviceView.toJSON());
122        
123        return response;
124    }
125    
126    private void _setRequestAttribute(String serviceId, String pageId, String zoneItemId, String zoneName)
127    {
128        Request request = ContextHelper.getRequest(_context);
129        
130        request.setAttribute(WebConstants.REQUEST_ATTR_SERVICE_ID, serviceId);
131        request.setAttribute(WebConstants.REQUEST_ATTR_PAGE_ID, pageId);
132        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM_ID, zoneItemId);
133        request.setAttribute(WebConstants.REQUEST_ATTR_ZONE_NAME, zoneName);
134    }
135    
136    /**
137     * Get the service parameter values
138     * @param zoneItemId the zone item id
139     * @param serviceId the service Id
140     * @return the values
141     */
142    @Callable
143    public Map<String, Object> getServiceParameterValues(String zoneItemId, String serviceId)
144    {
145        Service service = _serviceExtensionPoint.getExtension(serviceId);
146        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
147        
148        Map<String, Object> response = new HashMap<>();
149        Collection<ModelItem> serviceParameterDefinitions = service.getParameters().values();
150        ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters();
151
152        Map<String, Object> values = _getValues(serviceParameterDefinitions, dataHolder, "");
153        response.put("values", values);
154        
155        List<Map<String, Object>> repeaters = _getRepeaters(serviceParameterDefinitions, dataHolder, "");
156        response.put("repeaters", repeaters);
157        
158        return response;
159    }
160    
161    private Map<String, Object> _getValues(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix)
162    {
163        Map<String, Object> values = new HashMap<>();
164        
165        for (ModelItem item : items)
166        {
167            if (item instanceof ElementDefinition)
168            {
169                if (dataHolder.hasValue(item.getName()))
170                {
171                    ElementType type = ((ElementDefinition) item).getType();
172                    Object value = dataHolder.getValue(item.getName());
173                    
174                    values.put(prefix + item.getName(), type.valueToJSONForClient(value));
175                }
176            }
177            else if (item instanceof RepeaterDefinition)
178            {
179                if (dataHolder.hasValue(item.getName()))
180                {
181                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
182                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
183                    {
184                        String newPrefix = prefix + item.getName() + "[" + entry.getPosition() + "]/";
185                        values.putAll(_getValues(((RepeaterDefinition) item).getChildren(), entry, newPrefix));
186                    }
187                    values.put(prefix + item.getName(), new ArrayList<>());
188                }
189            }
190        }
191        
192        return values;
193    }
194    
195    private List<Map<String, Object>> _getRepeaters(Collection<ModelItem> items, ModelAwareDataHolder dataHolder, String prefix)
196    {
197        List<Map<String, Object>> results = new ArrayList<>();
198        
199        for (ModelItem item : items)
200        {
201            if (item instanceof RepeaterDefinition)
202            {
203                Map<String, Object> result = new HashMap<>();
204                
205                result.put("name", item.getName());
206                result.put("prefix", prefix);
207                
208                if (dataHolder.hasValue(item.getName()))
209                {
210                    ModelAwareRepeater repeater = dataHolder.getRepeater(item.getName());
211                    result.put("count", repeater.getSize());
212                    for (ModelAwareRepeaterEntry entry: repeater.getEntries())
213                    {
214                        StringBuilder newPrefix = new StringBuilder();
215                        newPrefix.append(prefix);
216                        newPrefix.append(item.getName()).append("[").append(entry.getPosition()).append("]/");
217                        results.addAll(_getRepeaters(((RepeaterDefinition) item).getChildren(), entry, newPrefix.toString()));
218                    }
219                }
220                else
221                {
222                    result.put("count", 0);
223                }
224                
225                results.add(result);
226            }
227        }
228        
229        return results;
230    }
231    
232    /**
233     * Add the service to the given zone on given page
234     * @param pageId The page identifier
235     * @param zoneName The zone name
236     * @param serviceId The identifier of the service to add
237     * @param parameterValues the service parameter values. Can be empty
238     * @return The result with the identifiers of updated page, zone and zone item
239     * @throws IOException if an error occurred while saving parameters
240     */
241    @Callable
242    public Map<String, Object> addService(String pageId, String zoneName, String serviceId, Map<String, Object> parameterValues) throws IOException
243    {
244        if (StringUtils.isEmpty(serviceId) || StringUtils.isEmpty(pageId) || StringUtils.isEmpty(zoneName))
245        {
246            throw new IllegalArgumentException("ServiceId, PageId or ZoneName is missing");
247        }
248        
249        // Check the service
250        Service service = null;
251        try
252        {
253            service = _serviceExtensionPoint.getExtension(serviceId);
254        }
255        catch (IllegalArgumentException e)
256        {
257            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
258        }
259        
260        try
261        {
262            Page page = _resolver.resolveById(pageId);
263            if (!(page instanceof ModifiablePage))
264            {
265                throw new IllegalArgumentException("Can not affect service on a non-modifiable page " + pageId);
266            }
267            
268            ModifiablePage modifiablePage = (ModifiablePage) page;
269            
270            if (page.getType() != PageType.CONTAINER)
271            {
272                throw new IllegalArgumentException("Can not affect service on a non-container page " + pageId);
273            }
274         
275            ModifiableZone zone;
276            if (page.hasZone(zoneName))
277            {
278                zone = modifiablePage.getZone(zoneName);
279            }
280            else
281            {
282                zone = modifiablePage.createZone(zoneName);
283            }
284            
285            ModifiableZoneItem zoneItem = zone.addZoneItem();
286            zoneItem.setType(ZoneType.SERVICE);
287            zoneItem.setServiceId(serviceId);
288            
289            ModifiableModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters();
290            
291            Map<String, List<I18nizableText>> allErrors = new HashMap<>();
292            _setParameterValues(serviceDataHolder, service, parameterValues, allErrors);
293            
294            Map<String, Object> results = new HashMap<>();
295            if (!allErrors.isEmpty())
296            {
297                results.put("errors", allErrors);
298                return results;
299            }
300            
301            modifiablePage.saveChanges();
302            
303            Map<String, Object> eventParams = new HashMap<>();
304            eventParams.put(ObservationConstants.ARGS_PAGE, page);
305            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
306            eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.SERVICE);
307            _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams));
308            
309            results.put("id", page.getId());
310            results.put("zoneitem-id", zoneItem.getId());
311            results.put("zone-name", zone.getName());
312            
313            return results;
314        }
315        catch (UnknownAmetysObjectException e)
316        {
317            throw new IllegalArgumentException("An error occured adding the service '" + serviceId + "' on the page '" + pageId + "'", e);
318        }
319    }
320    
321    /**
322     * Edit the parameter values of the given service
323     * @param zoneItemId The identifier of the zone item holding the service
324     * @param serviceId The service identifier
325     * @param parameterValues the service parameter values to update
326     * @return The result with the identifiers of updated page, zone and zone item
327     * @throws IOException if an error occurs while saving parameters
328     */
329    @Callable
330    public Map<String, Object> editServiceParameterValues(String zoneItemId, String serviceId, Map<String, Object> parameterValues) throws IOException
331    {
332        Service service = null;
333        try
334        {
335            service = _serviceExtensionPoint.getExtension(serviceId);
336        }
337        catch (IllegalArgumentException e)
338        {
339            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
340        }
341        
342        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
343        if (!(zoneItem instanceof ModifiableZoneItem))
344        {
345            throw new IllegalArgumentException("Can not configure service on a non-modifiable zone item " + zoneItemId);
346        }
347        
348        ModifiableZoneItem modifiableZoneItem = (ModifiableZoneItem) zoneItem;
349        
350        ModifiableModelAwareDataHolder serviceDataHolder = modifiableZoneItem.getServiceParameters();
351        
352        Map<String, List<I18nizableText>> allErrors = new HashMap<>();
353        _setParameterValues(serviceDataHolder, service, parameterValues, allErrors);
354        
355        Map<String, Object> results = new HashMap<>();
356        if (!allErrors.isEmpty())
357        {
358            results.put("errors", allErrors);
359            return results;
360        }
361        
362        modifiableZoneItem.saveChanges();
363        
364        Map<String, Object> eventParams = new HashMap<>();
365        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM, zoneItem);
366        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
367        _observationManager.notify(new Event(ObservationConstants.EVENT_SERVICE_MODIFIED, _currentUserProvider.getUser(), eventParams));
368        
369        results.put("id", zoneItem.getZone().getPage().getId());
370        results.put("zoneitem-id", zoneItem.getId());
371        results.put("zone-name", zoneItem.getZone().getName());
372        
373        return results;
374    }
375    
376    void _setParameterValues(ModifiableModelAwareDataHolder serviceDataHolder, Service service, Map<String, Object> values, Map<String, List<I18nizableText>> allErrors)
377    {
378        Map<String, ModelItem> definitions = service.getParameters();
379        for (ModelItem definition : definitions.values())
380        {
381            _getAndSaveParameter(definition, values, serviceDataHolder, "", allErrors);
382        }
383    }
384    
385    private void _getAndSaveParameter(ModelItem def, Map<String, Object> values, ModifiableModelAwareDataHolder dataHolder, String prefix, Map<String, List<I18nizableText>> allErrors)
386    {
387        if (def instanceof ElementDefinition)
388        {
389            ElementDefinition definition = (ElementDefinition) def;
390            Map<String, DefinitionAndValue> definitionAndValues = _getBrothersDefinitionAndValues(prefix, values, definition);
391            boolean isGroupSwitchOn = ModelHelper.isGroupSwitchOn(definition, values);
392            boolean isDisabled = ModelHelper.evaluateDisableConditions(definition.getDisableConditions(), definitionAndValues, getLogger());
393            
394            if (isGroupSwitchOn  && !isDisabled)
395            {
396            
397                Object submittedValue = values.get(prefix + definition.getName());
398                ElementType parameterType = definition.getType();
399                Object value = parameterType.fromJSONForClient(submittedValue);
400                
401                // TODO RUNTIME-2897: call the validateValue without boolean when multiple values are managed in enumerators
402                List<I18nizableText> errors = ModelHelper.validateValue(definition, value, false);
403                if (!errors.isEmpty())
404                {
405                    allErrors.put(definition.getName(), errors);
406                    return;
407                }
408        
409                if (!__SERVICE_PARAM_UNTOUCHED_BINARY.equals(value))
410                {
411                    dataHolder.setValue(definition.getName(), value);
412                }
413            }
414        }
415        else if (def instanceof RepeaterDefinition)
416        {
417            RepeaterDefinition definition = (RepeaterDefinition) def;
418            ModifiableModelAwareRepeater repeater = dataHolder.getRepeater(definition.getName(), true);
419            
420            // First move the entries according to the given previous positions
421            repeater.moveEntries(_getRepeaterPositionsMapping(values, prefix + definition.getName()));
422            
423            // Then save the entries' parameter values
424            for (ModifiableModelAwareRepeaterEntry entry : repeater.getEntries())
425            {
426                List<ModelItem> childrenDefinitions = definition.getChildren();
427                String newPrefix = prefix + definition.getName() + "[" + entry.getPosition() + "]" + ModelItem.ITEM_PATH_SEPARATOR;
428                for (ModelItem childDefinition : childrenDefinitions)
429                {
430                    _getAndSaveParameter(childDefinition, values, entry, newPrefix, allErrors);
431                }
432            }
433        }
434    }
435    
436    /**
437     * Get the definition and value pairs of the given definition brothers. The definition and value pairs are indexed by the parameter name
438     * @param prefix prefix to get the parameter values
439     * @param values all the parameter values
440     * @param definition the definition
441     * @return the definition and value pairs of the given definition brother
442     */
443    private Map<String, DefinitionAndValue> _getBrothersDefinitionAndValues(String prefix, Map<String, Object> values, ElementDefinition definition)
444    {
445        Map<String, DefinitionAndValue> definitionAndValues = new HashMap<>();
446        
447        ModelItemContainer definitionContainer = definition.getParent() != null ? definition.getParent() : definition.getModel();
448        Collection< ? extends ModelItem> definitionBrothers;
449        definitionBrothers = definitionContainer.getModelItems();
450        for (ModelItem brother : definitionBrothers)
451        {
452            Object value = values.get(prefix + brother.getName());
453            if (brother instanceof ElementDefinition)
454            {
455                DefinitionAndValue definitionAndValue = new DefinitionAndValue<>(null, brother, value);
456                definitionAndValues.put(brother.getName(), definitionAndValue);
457            }
458        }
459        
460        return definitionAndValues;
461    }
462    
463    private Map<Integer, Integer> _getRepeaterPositionsMapping(Map<String, Object> values, String repeaterPath)
464    {
465        Map<Integer, Integer> positionsMapping = new HashMap<>();
466
467        String sizeEntryName = "_" + repeaterPath + ModelItem.ITEM_PATH_SEPARATOR + "size";
468        if (values.containsKey(sizeEntryName))
469        {
470            int size = (int) values.get(sizeEntryName);
471            for (int position = 1; position <= size; position++)
472            {
473                String previousPositionEntryName = "_" + repeaterPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR + "previous-position";
474                if (values.containsKey(previousPositionEntryName))
475                {
476                    int previousPosition = (int) values.get("_" + repeaterPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR + "previous-position");
477                    positionsMapping.put(position, previousPosition);
478                }
479                else
480                {
481                    throw new IllegalArgumentException("The given values don't contain the previous position of the repeater entry '" + repeaterPath + "[" + position + "]" + "'.");
482                }
483            }
484            
485            return positionsMapping;
486        }
487        else
488        {
489            throw new IllegalArgumentException("The given values don't contain the size of the repeater at path '" + repeaterPath + "'.");
490        }
491    }
492}