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