001/*
002 *  Copyright 2015 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.util.HashMap;
019import java.util.Map;
020import java.util.Set;
021
022import javax.jcr.RepositoryException;
023
024import org.apache.avalon.framework.component.Component;
025import org.apache.avalon.framework.logger.AbstractLogEnabled;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.lang.ArrayUtils;
030import org.apache.commons.lang.StringUtils;
031import org.apache.commons.lang3.LocaleUtils;
032
033import org.ametys.cms.CmsConstants;
034import org.ametys.cms.contenttype.ContentType;
035import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.DefaultContent;
038import org.ametys.cms.repository.WorkflowAwareContent;
039import org.ametys.core.observation.Event;
040import org.ametys.core.observation.ObservationManager;
041import org.ametys.core.right.RightManager;
042import org.ametys.core.right.RightManager.RightResult;
043import org.ametys.core.ui.Callable;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.plugins.repository.AmetysObjectResolver;
047import org.ametys.plugins.repository.AmetysRepositoryException;
048import org.ametys.plugins.repository.UnknownAmetysObjectException;
049import org.ametys.plugins.repository.jcr.JCRAmetysObject;
050import org.ametys.plugins.workflow.support.WorkflowProvider;
051import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
052import org.ametys.web.ObservationConstants;
053import org.ametys.web.repository.content.SharedContent;
054import org.ametys.web.repository.content.WebContent;
055import org.ametys.web.repository.content.jcr.DefaultSharedContent;
056import org.ametys.web.repository.content.shared.SharedContentManager;
057import org.ametys.web.repository.page.ZoneItem.ZoneType;
058import org.ametys.web.service.Service;
059import org.ametys.web.service.ServiceExtensionPoint;
060
061/**
062 * DAO for manipulating {@link Zone} and {@link ZoneItem}
063 *
064 */
065public class ZoneDAO extends AbstractLogEnabled implements Serviceable, Component
066{
067    /** Avalon Role */
068    public static final String ROLE = ZoneDAO.class.getName();
069    
070    private AmetysObjectResolver _resolver;
071    private ObservationManager _observationManager;
072    private CurrentUserProvider _currentUserProvider;
073    private WorkflowProvider _workflowProvider;
074    private SharedContentManager _sharedContentManager;
075    private ContentTypesAssignmentHandler _cTypesAssignmentHandler;
076    private ServicesAssignmentHandler _serviceAssignmentHandler;
077    private ContentTypeExtensionPoint _cTypeEP;
078    private ServiceExtensionPoint _serviceEP;
079    private RightManager _rightManager;
080    
081    @Override
082    public void service(ServiceManager smanager) throws ServiceException
083    {
084        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
085        _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE);
086        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
087        _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE);
088        _sharedContentManager = (SharedContentManager) smanager.lookup(SharedContentManager.ROLE);
089        _cTypesAssignmentHandler = (ContentTypesAssignmentHandler) smanager.lookup(ContentTypesAssignmentHandler.ROLE);
090        _serviceAssignmentHandler = (ServicesAssignmentHandler) smanager.lookup(ServicesAssignmentHandler.ROLE);
091        _cTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
092        _serviceEP = (ServiceExtensionPoint) smanager.lookup(ServiceExtensionPoint.ROLE);
093        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
094    }
095    
096    /**
097     * Insert a shared content into a zone
098     * @param pageId The page id
099     * @param zoneName The zone name
100     * @param contentId The content id to insert
101     * @param viewName The view name
102     * @return The result map
103     */
104    @Callable
105    public Map<String, Object> addSharedContent (String pageId, String zoneName, String contentId, String viewName)
106    {
107        Map<String, Object> result = new HashMap<>();
108        
109        SitemapElement sitemapElement = _resolver.resolveById(pageId);
110        
111        if (!(sitemapElement instanceof ModifiableSitemapElement mSitemapElement))
112        {
113            throw new IllegalArgumentException("Can not add a shared content on a non-modifiable page " + pageId);
114        }
115        
116        if (sitemapElement.getTemplate() == null)
117        {
118            throw new IllegalArgumentException("Page '" + pageId + "' is not a container page: unable to add a shared content");
119        }
120        
121        ModifiableZone zone;
122        if (mSitemapElement.hasZone(zoneName))
123        {
124            zone = mSitemapElement.getZone(zoneName);
125        }
126        else
127        {
128            zone = mSitemapElement.createZone(zoneName);
129        }
130        
131        Content content = _resolver.resolveById(contentId);
132        String contentSiteName = null;
133        if (content instanceof WebContent)
134        {
135            contentSiteName = ((WebContent) content).getSiteName();
136        }
137        
138        // Check that the content type is an authorized content type.
139        Set<String> validCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(mSitemapElement, zoneName, true);
140        for (String contentTypeId : content.getTypes())
141        {
142            if (!validCTypes.contains(contentTypeId))
143            {
144                ContentType cType = _cTypeEP.getExtension(contentTypeId);
145                result.put("error", true);
146                result.put("invalid-contenttype", cType.getLabel());
147                return result;
148            }
149        }
150        
151        ZoneItem zoneItem = null;
152        if (contentSiteName == null || mSitemapElement.getSiteName().equals(contentSiteName))
153        {
154            // The content comes from the same site as the page: insert the content as a new zone item.
155            zoneItem = addContentReference(zone, content, viewName);
156        }
157        else
158        {
159            // The content is from a different site: create a shared content in the zone.
160            if (!(content instanceof DefaultContent) || content instanceof SharedContent)
161            {
162                throw new IllegalArgumentException("The source content must be a DefaultContent but not a SharedContent.");
163            }
164            
165            DefaultContent defaultContent = (DefaultContent) content;
166            
167            if (!ArrayUtils.contains(defaultContent.getAllLabels(), CmsConstants.LIVE_LABEL))
168            {
169                result.put("error", true);
170                result.put("unpublished-content", defaultContent.getTitle(LocaleUtils.toLocale(mSitemapElement.getSitemapName())));
171                return result;
172            }
173            
174            zoneItem = createSharedContent(zone, defaultContent, viewName);
175        }
176        
177        mSitemapElement.saveChanges();
178        
179        String zoneItemId = zoneItem.getId();
180        result.put("zoneItemId", zoneItemId);
181        
182        Map<String, Object> eventParams = new HashMap<>();
183        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, mSitemapElement);
184        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
185        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT);
186        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent());
187        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_ADDED, _currentUserProvider.getUser(), eventParams));
188        
189        return result;
190    }
191    
192    /**
193     * Add the given content as a zone item in the given zone.
194     * @param zone the zone to add the content in.
195     * @param content the content to add.
196     * @param viewName the view name.
197     * @return the added zone item
198     */
199    protected ZoneItem addContentReference(ModifiableZone zone, Content content, String viewName)
200    {
201        ModifiableZoneItem zoneItem = zone.addZoneItem();
202        zoneItem.setType(ZoneType.CONTENT);
203        
204        zoneItem.setContent(content);
205        zoneItem.setViewName(viewName);
206        
207        return zoneItem;
208    }
209    
210    /**
211     * Create a shared content referencing the given content and add the shared one to the zone.
212     * @param zone the zone to create the shared content in.
213     * @param originalContent the original content.
214     * @param viewName the view name.
215     * @return the added zone item
216     */
217    protected ZoneItem createSharedContent(ModifiableZone zone, DefaultContent originalContent, String viewName)
218    {
219        ModifiableZoneItem zoneItem = zone.addZoneItem();
220        zoneItem.setType(ZoneType.CONTENT);
221        
222        DefaultSharedContent content = _sharedContentManager.createSharedContent(zone.getSitemapElement().getSite(), originalContent);
223        
224        zoneItem.setContent(content);
225        zoneItem.setViewName(viewName);
226        
227        return zoneItem;
228    }
229    
230    /**
231     * Remove a zone item from page
232     * @param zoneItemId The id of zone item to delete
233     */
234    @Callable
235    public void removeZoneItem (String zoneItemId)
236    {
237        ZoneItem zoneItem = _resolver.resolveById(zoneItemId);
238        
239        if (!(zoneItem instanceof ModifiableZoneItem))
240        {
241            throw new IllegalArgumentException("Can not remove a non-modifiable zone item " + zoneItemId);
242        }
243        
244        ModifiableSitemapElement sitemapElement = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement();
245        
246        if (sitemapElement instanceof LockablePage lockablePage && lockablePage.isLocked())
247        {
248            throw new IllegalArgumentException("Can not remove a zone item from a locked page " + lockablePage.getId());
249        }
250        
251        Map<String, Object> eventParams = new HashMap<>();
252        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
253        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
254        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType());
255        if (zoneItem.getType() == ZoneType.CONTENT)
256        {
257            // In case of a zone item of type content, provide it.
258            // There should be no problem as the observers are processed synchronously and,
259            // if the user want it to be deleted, it will happen in a future client call.
260            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent());
261        }
262        else if (zoneItem.getType() == ZoneType.SERVICE)
263        {
264            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, zoneItem.getServiceId());
265        }
266        
267        ((ModifiableZoneItem) zoneItem).remove();
268        sitemapElement.saveChanges();
269        
270        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _currentUserProvider.getUser(), eventParams));
271    }
272    
273    /**
274     * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem}
275     * @param id The id of page or zone item
276     * @return The list of unreferenced contents
277     */
278    @Callable
279    public Map<String, Object> getZoneItemElementInfo(String id)
280    {
281        ZoneItem zoneItem = _resolver.resolveById(id);
282        
283        Map<String, Object> info = new HashMap<>();
284        info.put("type", zoneItem.getType().toString().toLowerCase());
285        
286        Content content = getContent(zoneItem);
287        if (content != null)
288        {
289            info.put("id", content.getId());
290            info.put("name", content.getName());
291            info.put("title", content.getTitle(LocaleUtils.toLocale(zoneItem.getZone().getSitemapElement().getSitemapName())));
292            info.put("isNew", _isNew(content));
293            info.put("isShared", content instanceof SharedContent);
294            info.put("hasShared", _sharedContentManager.hasSharedContents(content));
295            info.put("isReferenced", _isReferenced(content));
296        }
297        else
298        {
299            Service service = getService(zoneItem);
300            if (service != null)
301            {
302                info.put("id", service.getId());
303                info.put("label", service.getLabel());
304                info.put("url", service.getURL());
305            }
306        }
307        
308        return info;
309    }
310    
311    /**
312     * Get the content of a zone item. Can be null if zone item is a service zone item.
313     * @param zoneItem The zone item
314     * @return The content or null if zone item is a service zone item.
315     */
316    public Content getContent (ZoneItem zoneItem)
317    {
318        if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT)
319        {
320            return zoneItem.getContent();
321        }
322        return null;
323    }
324    
325    private boolean _isReferenced (Content content)
326    {
327        return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1;
328    }
329    
330    private boolean _isNew (Content content)
331    {
332        boolean isNew = false;
333        if (content instanceof WorkflowAwareContent)
334        {
335            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
336            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
337            
338            long workflowId = waContent.getWorkflowId();
339            isNew = workflow.getHistorySteps(workflowId).isEmpty();
340        }
341        return isNew;
342    }
343    
344    /**
345     * Get the service of a zone item. Can be null if zone item is not a service zone item.
346     * @param zoneItem The zone item
347     * @return The service or null if zone item is not a service zone item.
348     */
349    public Service getService (ZoneItem zoneItem)
350    {
351        if (zoneItem.getType() == ZoneItem.ZoneType.SERVICE)
352        {
353            return _serviceEP.getExtension(zoneItem.getServiceId());
354        }
355        return null;
356    }
357    
358    /**
359     * Move a zone item of a page before/after another zone item of the same page
360     * @param zoneItemId The identifier of the zone item to move
361     * @param zoneName The destination zone name
362     * @param beforeOrAfter true if before or false after
363     * @param beforeOrAfterItemId The target zone item can be null
364     * @param pageId The concerned page id
365     * @return true when success
366     * @throws UnknownAmetysObjectException If an error occurred
367     * @throws AmetysRepositoryException If an error occurred
368     * @throws RepositoryException If an error occurred
369     */
370    @Callable
371    public boolean moveZoneItemTo(String zoneItemId, String zoneName, boolean beforeOrAfter, String beforeOrAfterItemId, String pageId) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException
372    {
373        UserIdentity user = _currentUserProvider.getUser();
374        
375        ModifiableSitemapElement sitemapElement = _resolver.resolveById(pageId);
376        
377        Zone zone;
378        if (!sitemapElement.hasZone(zoneName))
379        {
380            zone = sitemapElement.createZone(zoneName);
381        }
382        else
383        {
384            zone = sitemapElement.getZone(zoneName);
385        }
386        
387        ZoneItem zoneItem = sitemapElement instanceof JCRAmetysObject ? (ZoneItem) _resolver.resolveById(zoneItemId, ((JCRAmetysObject) sitemapElement).getNode().getSession()) : (ZoneItem) _resolver.resolveById(zoneItemId);
388        
389        _checkZoneItem(sitemapElement, zoneName, zoneItem, user);
390        
391        _checkArguments(zoneItemId, zoneName, pageId, user, sitemapElement, zone, zoneItem);
392        
393        ((ModifiableZoneItem) zoneItem).moveTo(zone, false);
394        
395        // For the moment the zoneItem is at the end
396        if (StringUtils.isNotBlank(beforeOrAfterItemId))
397        {
398            if (beforeOrAfter)
399            {
400                ZoneItem futureFollowingZoneItem = _resolver.resolveById(beforeOrAfterItemId);
401                ((ModifiableZoneItem) zoneItem).orderBefore(futureFollowingZoneItem);
402            }
403            else
404            {
405                boolean isNext = false;
406                for (ZoneItem zi : zone.getZoneItems())
407                {
408                    if (isNext)
409                    {
410                        ((ModifiableZoneItem) zoneItem).orderBefore(zi);
411                        break;
412                    }
413                    else if (beforeOrAfterItemId.equals(zi.getId()))
414                    {
415                        isNext = true;
416                    }
417                }
418            }
419        }
420        
421        ModifiableSitemapElement zoneItemPage = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement();
422        if (zoneItemPage.needsSave())
423        {
424            zoneItemPage.saveChanges();
425        }
426        
427        Map<String, Object> eventParams = new HashMap<>();
428        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
429        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
430        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType());
431        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_MOVED, user, eventParams));
432        
433        return true;
434    }
435    
436    /**
437     * Check if the zone item can be moved to the given zone of the given page.
438     * @param page the page.
439     * @param zoneName the zone name.
440     * @param zoneItem the zone item to move.
441     * @param user the user
442     */
443    protected void _checkZoneItem(ModifiableSitemapElement page, String zoneName, ZoneItem zoneItem, UserIdentity user)
444    {
445        if (ZoneType.CONTENT.equals(zoneItem.getType()))
446        {
447            String[] contentTypes = zoneItem.getContent().getTypes();
448            
449            Set<String> availableCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(page, zoneName, true);
450            
451            for (String contentType : contentTypes)
452            {
453                if (!availableCTypes.contains(contentType))
454                {
455                    throw new IllegalArgumentException("The user '" + user + " illegally tried to move a content of type '" + contentType + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'.");
456                }
457            }
458        }
459        else if (ZoneType.SERVICE.equals(zoneItem.getType()))
460        {
461            String serviceId = zoneItem.getServiceId();
462            
463            Set<String> services = _serviceAssignmentHandler.getAvailableServices(page, zoneName);
464            if (!services.contains(serviceId))
465            {
466                throw new IllegalArgumentException("The user '" + user + " illegally tried to move a service of id '" + serviceId + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'.");
467            }
468        }
469    }
470    
471    private void _checkArguments(String zoneItemId, String zoneName, String pageId, UserIdentity user, ModifiableSitemapElement page, Zone zone, ZoneItem zoneItem)
472    {
473        if (_rightManager.hasRight(user, "Web_Rights_Page_OrganizeZoneItem", page) != RightResult.RIGHT_ALLOW)
474        {
475            throw new IllegalArgumentException("User '" + user + "' tryed without convinient privileges to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !");
476        }
477        else if (zoneItem == null)
478        {
479            throw new IllegalArgumentException("User '" + user + "' tryed to move unexisting zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !");
480        }
481        else if (zone == null)
482        {
483            throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to an unexisting zone '" + zoneName + "' of the page '" + pageId + "' !");
484        }
485        else if (!(zone instanceof ModifiableZone))
486        {
487            throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone is not modifiable !");
488        }
489        else if (!(zoneItem instanceof ModifiableZoneItem))
490        {
491            throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' but this zone item is not modifiable !");
492        }
493    }
494}