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.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Optional;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.context.Context;
028import org.apache.avalon.framework.context.ContextException;
029import org.apache.avalon.framework.context.Contextualizable;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.cocoon.ProcessingException;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.lang3.tuple.ImmutablePair;
038import org.apache.commons.lang3.tuple.Pair;
039
040import org.ametys.core.observation.Event;
041import org.ametys.core.observation.ObservationManager;
042import org.ametys.core.ui.Callable;
043import org.ametys.core.user.CurrentUserProvider;
044import org.ametys.plugins.repository.AmetysObjectResolver;
045import org.ametys.plugins.repository.UnknownAmetysObjectException;
046import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
047import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
048import org.ametys.runtime.i18n.I18nizableText;
049import org.ametys.runtime.model.DefinitionContext;
050import org.ametys.runtime.model.ModelItem;
051import org.ametys.runtime.model.View;
052import org.ametys.runtime.model.ViewHelper;
053import org.ametys.runtime.model.ViewItem;
054import org.ametys.runtime.model.ViewItemContainer;
055import org.ametys.runtime.model.disableconditions.DisableCondition.OPERATOR;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057import org.ametys.web.ObservationConstants;
058import org.ametys.web.WebConstants;
059import org.ametys.web.parameters.ParametersManager;
060import org.ametys.web.parameters.view.ViewParametersDAO;
061import org.ametys.web.parameters.view.ViewParametersManager;
062import org.ametys.web.parameters.view.ViewParametersModel;
063import org.ametys.web.repository.page.ZoneItem.ZoneType;
064import org.ametys.web.service.Service;
065import org.ametys.web.service.ServiceExtensionPoint;
066
067/**
068 * Class containing callables to retrieve and configure service parameters
069 */
070public class ZoneItemManager extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
071{
072    /** Avalon Role */
073    public static final String ROLE = ZoneItemManager.class.getName();
074    
075    /** Constant for untouched binary metadata. */
076    protected static final String __SERVICE_PARAM_UNTOUCHED_BINARY = "untouched";
077    
078    private ServiceExtensionPoint _serviceExtensionPoint;
079    private AmetysObjectResolver _resolver;
080    private ObservationManager _observationManager;
081    private CurrentUserProvider _currentUserProvider;
082    private ParametersManager _parametersManager;
083    private ViewParametersManager _viewParametersManager;
084    private Context _context;
085    private PageDAO _pageDAO;
086    
087    public void service(ServiceManager manager) throws ServiceException
088    {
089        _serviceExtensionPoint = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
090        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
091        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
092        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
093        _parametersManager = (ParametersManager) manager.lookup(ParametersManager.ROLE);
094        _viewParametersManager = (ViewParametersManager) manager.lookup(ViewParametersManager.ROLE);
095        _pageDAO = (PageDAO) manager.lookup(PageDAO.ROLE);
096    }
097    
098    public void contextualize(Context context) throws ContextException
099    {
100        _context = context;
101    }
102    
103    /**
104     * Retrieves the parameter definitions of the given service
105     * @param serviceId Identifier of the service
106     * @param pageId the page id
107     * @param zoneItemId the zone item id
108     * @param zoneName the zone name
109     * @return the parameter definitions
110     * @throws ProcessingException if an error occurs
111     */
112    @Callable
113    public Map<String, Object> getServiceParameterDefinitions(String serviceId, String pageId, String zoneItemId, String zoneName) throws ProcessingException
114    {
115        _setRequestAttribute(serviceId, pageId, zoneItemId, zoneName);
116        
117        Map<String, Object> response = new HashMap<>();
118        
119        Service service = _serviceExtensionPoint.getExtension(serviceId);
120        
121        response.put("id", serviceId);
122        response.put("url", service.getURL());
123        response.put("label", service.getLabel());
124        response.put("height", service.getCreationBoxHeight());
125        response.put("width", service.getCreationBoxWidth());
126        
127        // Clone the view to not record in the service view the added view parameters
128        View clonedView = _cloneView(service.getView());
129        
130        SitemapElement sitemapElement = _resolver.resolveById(pageId);
131        
132        Map<String, ViewParametersModel> serviceViewParametersModels = _viewParametersManager.getServiceViewParametersModels(sitemapElement.getSite().getSkinId(), serviceId);
133        
134        Pair<ViewItemContainer, Integer> containerAndIndex = _getXSLTViewItemContainerAndIndex(clonedView);
135        if (containerAndIndex != null)
136        {
137            for (String viewName : serviceViewParametersModels.keySet())
138            {
139                ViewParametersModel viewParameters = serviceViewParametersModels.get(viewName);
140                Collection< ? extends ModelItem> modelItems = viewParameters.getModelItems();
141                
142                String prefix = ViewParametersDAO.PREFIX_SERVICE + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR + _viewParametersManager.normalizeViewName(viewName) + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR;
143                Optional<Integer> index = Optional.of(containerAndIndex.getRight() + 1); //add 1 to the XSLT parameter index to include the view parameters after the XSLT one
144                List<ModelItem> includeModelItems = _viewParametersManager.includeModelItems(modelItems, prefix, containerAndIndex.getLeft(), index); //add 1 to the XSL
145                _viewParametersManager.setDisableConditions(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME, OPERATOR.NEQ, viewName, includeModelItems);
146            }
147        }
148        
149        response.put("parameters", clonedView.toJSON(DefinitionContext.newInstance().withEdition(true)));
150        
151        return response;
152    }
153    
154    private View _cloneView(View serviceView)
155    {
156        View clonedView = new View();
157        serviceView.copyTo(clonedView);
158        clonedView.addViewItems(ViewHelper.copyViewItems(serviceView.getViewItems()));
159        return clonedView;
160    }
161    
162    private Pair<ViewItemContainer, Integer> _getXSLTViewItemContainerAndIndex(ViewItemContainer viewItemContainer)
163    {
164        List<ViewItem> viewItems = viewItemContainer.getViewItems();
165        for (int i = 0; i < viewItems.size(); i++)
166        {
167            ViewItem viewItem = viewItems.get(i);
168
169            if (ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME.equals(viewItem.getName()))
170            {
171                return ImmutablePair.of(viewItemContainer, i);
172            }
173            else if (viewItem instanceof ViewItemContainer)
174            {
175                Pair<ViewItemContainer, Integer> xsltContainerAndIndex = _getXSLTViewItemContainerAndIndex((ViewItemContainer) viewItem);
176                if (xsltContainerAndIndex != null)
177                {
178                    return xsltContainerAndIndex;
179                }
180            }
181        }
182        
183        return null;
184    }
185    
186    private void _setRequestAttribute(String serviceId, String pageId, String zoneItemId, String zoneName)
187    {
188        Request request = ContextHelper.getRequest(_context);
189        
190        request.setAttribute(WebConstants.REQUEST_ATTR_SERVICE_ID, serviceId);
191        request.setAttribute(WebConstants.REQUEST_ATTR_PAGE_ID, pageId);
192        request.setAttribute(WebConstants.REQUEST_ATTR_ZONEITEM_ID, zoneItemId);
193        request.setAttribute(WebConstants.REQUEST_ATTR_ZONE_NAME, zoneName);
194    }
195    
196    /**
197     * Get the service parameter values
198     * @param zoneItemId the zone item id
199     * @param serviceId the service Id
200     * @return the values
201     */
202    @Callable
203    public Map<String, Object> getServiceParameterValues(String zoneItemId, String serviceId)
204    {
205        Service service = _serviceExtensionPoint.getExtension(serviceId);
206        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
207        Zone zone = zoneItem.getZone();
208        
209        _setRequestAttribute(serviceId, zone.getSitemapElement().getId(), zoneItemId, zone.getName());
210        
211        Map<String, Object> response = new HashMap<>();
212        Collection<ModelItem> serviceParameterDefinitions = service.getParameters().values();
213        ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters();
214
215        Map<String, Object> values = _parametersManager.getParametersValues(serviceParameterDefinitions, dataHolder, "");
216        values.putAll(_getServiceViewParametersValues(zoneItem));
217        
218        response.put("values", values);
219        
220        List<Map<String, Object>> repeaters = _parametersManager.getRepeatersValues(serviceParameterDefinitions, dataHolder, "");
221        response.put("repeaters", repeaters);
222        
223        return response;
224    }
225    
226    /**
227     * Get the service view parameters values
228     * @param zoneItem the zone item
229     * @return the values
230     */
231    protected Map<String, Object> _getServiceViewParametersValues(ZoneItem zoneItem)
232    {
233        Map<String, Object> values = new HashMap<>();
234        
235        String skinId = zoneItem.getZone()
236                .getSitemapElement()
237                .getSite()
238                .getSkinId();
239        
240        Map<String, ViewParametersModel> serviceViewParametersModels = _viewParametersManager.getServiceViewParametersModels(skinId, zoneItem.getServiceId());
241        for (String viewName : serviceViewParametersModels.keySet())
242        {
243            ViewParametersModel serviceViewParameters = serviceViewParametersModels.get(viewName);
244            ModelAwareDataHolder serviceViewParametersHolder = zoneItem.getServiceViewParametersHolder(viewName);
245            
246            Map<String, Object> serviceValues = _parametersManager.getParametersValues(serviceViewParameters.getModelItems(), serviceViewParametersHolder, "");
247            String prefix = ViewParametersDAO.PREFIX_SERVICE + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR + _viewParametersManager.normalizeViewName(viewName) + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR;
248            values.putAll(_parametersManager.addPrefixToParameters(serviceValues, prefix));
249        }
250        
251        return values;
252    }
253    
254    /**
255     * Add the service to the given zone on given page
256     * @param pageId The page identifier
257     * @param zoneName The zone name
258     * @param serviceId The identifier of the service to add
259     * @param parameterValues the service parameter values. Can be empty
260     * @return The result with the identifiers of updated page, zone and zone item
261     * @throws IOException if an error occurred while saving parameters
262     */
263    @Callable
264    public Map<String, Object> addService(String pageId, String zoneName, String serviceId, Map<String, Object> parameterValues) throws IOException
265    {
266        if (StringUtils.isEmpty(serviceId) || StringUtils.isEmpty(pageId) || StringUtils.isEmpty(zoneName))
267        {
268            throw new IllegalArgumentException("ServiceId, PageId or ZoneName is missing");
269        }
270        
271        // Check the service
272        Service service = null;
273        try
274        {
275            service = _serviceExtensionPoint.getExtension(serviceId);
276        }
277        catch (IllegalArgumentException e)
278        {
279            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
280        }
281        
282        try
283        {
284            SitemapElement sitemapElement = _resolver.resolveById(pageId);
285            if (!(sitemapElement instanceof ModifiableSitemapElement modifiableSitemapElement))
286            {
287                throw new IllegalArgumentException("Can not affect service on a non-modifiable page " + pageId);
288            }
289            
290            if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked())
291            {
292                throw new IllegalArgumentException("Can not affect service on a locked page " + pageId);
293            }
294            
295            if (sitemapElement.getTemplate() == null)
296            {
297                throw new IllegalArgumentException("Can not affect service on a non-container page " + pageId);
298            }
299            
300            if (!_getAllowedServicesId(pageId, zoneName).contains(serviceId))
301            {
302                throw new IllegalArgumentException("Can not affect service '" + serviceId + "' on a zone '" + zoneName + "'");
303            }
304         
305            ModifiableZone zone;
306            if (modifiableSitemapElement.hasZone(zoneName))
307            {
308                zone = modifiableSitemapElement.getZone(zoneName);
309            }
310            else
311            {
312                zone = modifiableSitemapElement.createZone(zoneName);
313            }
314            
315            ModifiableZoneItem zoneItem = zone.addZoneItem();
316            zoneItem.setType(ZoneType.SERVICE);
317            zoneItem.setServiceId(serviceId);
318            
319            Map<String, List<I18nizableText>> allErrors = _setParameterValues(parameterValues, service, zoneItem);
320            
321            Map<String, Object> results = new HashMap<>();
322            if (!allErrors.isEmpty())
323            {
324                results.put("errors", allErrors);
325                return results;
326            }
327            
328            modifiableSitemapElement.saveChanges();
329            
330            Map<String, Object> eventParams = new HashMap<>();
331            eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
332            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
333            eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.SERVICE);
334            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, serviceId);
335            _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams));
336            
337            results.put("id", sitemapElement.getId());
338            results.put("zoneitem-id", zoneItem.getId());
339            results.put("zone-name", zone.getName());
340            
341            return results;
342        }
343        catch (UnknownAmetysObjectException e)
344        {
345            throw new IllegalArgumentException("An error occured adding the service '" + serviceId + "' on the page '" + pageId + "'", e);
346        }
347    }
348
349    private List<String> _getAllowedServicesId(String pageId, String zoneName)
350    {
351        return _pageDAO.getAvailableServices(pageId, zoneName).stream()
352        .map(info -> (String) info.get("id"))
353        .collect(Collectors.toList());
354    }
355    
356    /**
357     * Edit the parameter values of the given service
358     * @param zoneItemId The identifier of the zone item holding the service
359     * @param serviceId The service identifier
360     * @param parameterValues the service parameter values to update
361     * @return The result with the identifiers of updated page, zone and zone item
362     * @throws IOException if an error occurs while saving parameters
363     */
364    @Callable
365    public Map<String, Object> editServiceParameterValues(String zoneItemId, String serviceId, Map<String, Object> parameterValues) throws IOException
366    {
367        Service service = null;
368        try
369        {
370            service = _serviceExtensionPoint.getExtension(serviceId);
371        }
372        catch (IllegalArgumentException e)
373        {
374            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
375        }
376        
377        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
378        if (!(zoneItem instanceof ModifiableZoneItem))
379        {
380            throw new IllegalArgumentException("Can not configure service on a non-modifiable zone item " + zoneItemId);
381        }
382        
383        if (zoneItem.getZone().getSitemapElement() instanceof LockablePage lockablePage && lockablePage.isLocked())
384        {
385            throw new IllegalArgumentException("Can not configure service on a locked page '/" + lockablePage.getSitemapName() + "/" + lockablePage.getPathInSitemap() + "'");
386        }
387        
388        ModifiableZoneItem modifiableZoneItem = (ModifiableZoneItem) zoneItem;
389        
390        Map<String, List<I18nizableText>> allErrors = _setParameterValues(parameterValues, service, modifiableZoneItem);
391        
392        Map<String, Object> results = new HashMap<>();
393        if (!allErrors.isEmpty())
394        {
395            results.put("errors", allErrors);
396            return results;
397        }
398        
399        modifiableZoneItem.saveChanges();
400        
401        Map<String, Object> eventParams = new HashMap<>();
402        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM, zoneItem);
403        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
404        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, serviceId);
405        _observationManager.notify(new Event(ObservationConstants.EVENT_SERVICE_MODIFIED, _currentUserProvider.getUser(), eventParams));
406        
407        results.put("id", zoneItem.getZone().getSitemapElement().getId());
408        results.put("zoneitem-id", zoneItem.getId());
409        results.put("zone-name", zoneItem.getZone().getName());
410        
411        return results;
412    }
413
414    /**
415     * Set the parameter values for the service (with view parameters)
416     * @param parameterValues the parameter values
417     * @param service the service
418     * @param zoneItem the zone item
419     * @return the map of error
420     */
421    protected Map<String, List<I18nizableText>> _setParameterValues(Map<String, Object> parameterValues, Service service, ModifiableZoneItem zoneItem)
422    {
423        ModifiableModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters();
424        Map<String, ModelItem> definitions = service.getParameters();
425        Map<String, List<I18nizableText>> allErrors = _parametersManager.setParameterValues(serviceDataHolder, definitions.values(), parameterValues);
426        
427        if (parameterValues.containsKey(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME))
428        {
429            String viewName = (String) parameterValues.get(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME);
430            
431            String skinId = zoneItem.getZone()
432                    .getSitemapElement()
433                    .getSite()
434                    .getSkinId();
435            Optional<ViewParametersModel> serviceViewParametersModel = _viewParametersManager.getServiceViewParametersModel(skinId, service.getId(), viewName);
436            
437            if (serviceViewParametersModel.isPresent())
438            {
439                ModifiableModelAwareDataHolder serviceParametersHolder = zoneItem.getServiceViewParametersHolder(viewName);
440                String prefix = ViewParametersDAO.PREFIX_SERVICE + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR + _viewParametersManager.normalizeViewName(viewName) + ViewParametersDAO.MODEL_ITEM_NAME_SEPARATOR;
441                Map<String, Object> viewParametersValues = _parametersManager.getParametersStartWithPrefix(parameterValues, prefix);
442                allErrors.putAll(_parametersManager.setParameterValues(serviceParametersHolder, serviceViewParametersModel.get().getModelItems(), viewParametersValues));
443            }
444        }
445        
446        return allErrors;
447    }
448    
449    /**
450     * Paste the given service in the given zone
451     * @param srcZoneItemId The identifier of the zone item holding the original service
452     * @param serviceId The service identifier
453     * @param targetPageId The identifier of the page where to paste the service
454     * @param targetZoneName The zone name  where to paste the service
455     * @return The result with the identifiers of updated page, zone and zone item
456     * @throws IOException if an error occurs while saving parameters
457     */
458    @Callable
459    public Map<String, Object> pasteService(String srcZoneItemId, String serviceId, String targetPageId, String targetZoneName) throws IOException
460    {
461        if (StringUtils.isEmpty(serviceId) || StringUtils.isEmpty(targetPageId) || StringUtils.isEmpty(targetZoneName))
462        {
463            throw new IllegalArgumentException("ServiceId, PageId or ZoneName is missing");
464        }
465        
466        // Check the service
467        try
468        {
469            _serviceExtensionPoint.getExtension(serviceId);
470        }
471        catch (IllegalArgumentException e)
472        {
473            throw new IllegalArgumentException("Service with id '" + serviceId + "' does not exist", e);
474        }
475        
476        try
477        {
478            SitemapElement sitemapElement = _resolver.resolveById(targetPageId);
479            if (!(sitemapElement instanceof ModifiableSitemapElement modifiableSitemapElement))
480            {
481                throw new IllegalArgumentException("Can not affect service on a non-modifiable page " + targetPageId);
482            }
483            
484            if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked())
485            {
486                throw new IllegalArgumentException("Can not affect service on a locked page " + targetPageId);
487            }
488            
489            if (sitemapElement.getTemplate() == null)
490            {
491                throw new IllegalArgumentException("Can not affect service on a non-container page " + targetPageId);
492            }
493            
494            if (!_getAllowedServicesId(targetPageId, targetZoneName).contains(serviceId))
495            {
496                throw new IllegalArgumentException("Can not affect service '" + serviceId + "' on a zone '" + targetZoneName + "'");
497            }
498         
499            ModifiableZone zone;
500            if (modifiableSitemapElement.hasZone(targetZoneName))
501            {
502                zone = modifiableSitemapElement.getZone(targetZoneName);
503            }
504            else
505            {
506                zone = modifiableSitemapElement.createZone(targetZoneName);
507            }
508            
509            ModifiableZoneItem zoneItem = zone.addZoneItem();
510            zoneItem.setType(ZoneType.SERVICE);
511            zoneItem.setServiceId(serviceId);
512            
513            ZoneItem srcService = _resolver.resolveById(srcZoneItemId);
514            ModelAwareDataHolder serviceDataHolder = srcService.getServiceParameters();
515            serviceDataHolder.copyTo(zoneItem.getServiceParameters());
516            
517            if (serviceDataHolder.hasDefinition(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME))
518            {
519                String viewName = serviceDataHolder.getValue(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME);
520                
521                // Copy of view parameters
522                srcService.getServiceViewParametersHolder(viewName).copyTo(zoneItem.getServiceViewParametersHolder(viewName));
523            }
524            Map<String, Object> results = new HashMap<>();
525            
526            modifiableSitemapElement.saveChanges();
527            
528            Map<String, Object> eventParams = new HashMap<>();
529            eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
530            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
531            eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.SERVICE);
532            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, serviceId);
533            _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams));
534            
535            results.put("id", sitemapElement.getId());
536            results.put("zoneitem-id", zoneItem.getId());
537            results.put("zone-name", zone.getName());
538            
539            return results;
540        }
541        catch (UnknownAmetysObjectException e)
542        {
543            throw new IllegalArgumentException("An error occured adding the service '" + serviceId + "' on the page '" + targetPageId + "'", e);
544        }
545    }
546}