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.Locale;
020import java.util.Map;
021import java.util.Set;
022
023import javax.jcr.RepositoryException;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.logger.AbstractLogEnabled;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.lang.ArrayUtils;
031import org.apache.commons.lang.StringUtils;
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(new Locale(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        Map<String, Object> eventParams = new HashMap<>();
247        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
248        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
249        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType());
250        if (zoneItem.getType() == ZoneType.CONTENT)
251        {
252            // In case of a zone item of type content, provide it.
253            // There should be no problem as the observers are processed synchronously and,
254            // if the user want it to be deleted, it will happen in a future client call.
255            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_CONTENT, zoneItem.getContent());
256        }
257        else if (zoneItem.getType() == ZoneType.SERVICE)
258        {
259            eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_SERVICE, zoneItem.getServiceId());
260        }
261        
262        ((ModifiableZoneItem) zoneItem).remove();
263        sitemapElement.saveChanges();
264        
265        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_DELETED, _currentUserProvider.getUser(), eventParams));
266    }
267    
268    /**
269     * Get the unreferenced contents of a {@link Page} or a {@link ZoneItem}
270     * @param id The id of page or zone item
271     * @return The list of unreferenced contents
272     */
273    @Callable
274    public Map<String, Object> getZoneItemElementInfo(String id)
275    {
276        ZoneItem zoneItem = _resolver.resolveById(id);
277        
278        Map<String, Object> info = new HashMap<>();
279        info.put("type", zoneItem.getType().toString().toLowerCase());
280        
281        Content content = getContent(zoneItem);
282        if (content != null)
283        {
284            info.put("id", content.getId());
285            info.put("name", content.getName());
286            info.put("title", content.getTitle(new Locale(zoneItem.getZone().getSitemapElement().getSitemapName())));
287            info.put("isNew", _isNew(content));
288            info.put("isShared", content instanceof SharedContent);
289            info.put("hasShared", _sharedContentManager.hasSharedContents(content));
290            info.put("isReferenced", _isReferenced(content));
291        }
292        else
293        {
294            Service service = getService(zoneItem);
295            if (service != null)
296            {
297                info.put("id", service.getId());
298                info.put("label", service.getLabel());
299                info.put("url", service.getURL());
300            }
301        }
302        
303        return info;
304    }
305    
306    /**
307     * Get the content of a zone item. Can be null if zone item is a service zone item.
308     * @param zoneItem The zone item
309     * @return The content or null if zone item is a service zone item.
310     */
311    public Content getContent (ZoneItem zoneItem)
312    {
313        if (zoneItem.getType() == ZoneItem.ZoneType.CONTENT)
314        {
315            return zoneItem.getContent();
316        }
317        return null;
318    }
319    
320    private boolean _isReferenced (Content content)
321    {
322        return content instanceof WebContent && ((WebContent) content).getReferencingPages().size() > 1;
323    }
324    
325    private boolean _isNew (Content content)
326    {
327        boolean isNew = false;
328        if (content instanceof WorkflowAwareContent)
329        {
330            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
331            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
332            
333            long workflowId = waContent.getWorkflowId();
334            isNew = workflow.getHistorySteps(workflowId).isEmpty();
335        }
336        return isNew;
337    }
338    
339    /**
340     * Get the service of a zone item. Can be null if zone item is not a service zone item.
341     * @param zoneItem The zone item
342     * @return The service or null if zone item is not a service zone item.
343     */
344    public Service getService (ZoneItem zoneItem)
345    {
346        if (zoneItem.getType() == ZoneItem.ZoneType.SERVICE)
347        {
348            return _serviceEP.getExtension(zoneItem.getServiceId());
349        }
350        return null;
351    }
352    
353    /**
354     * Move a zone item of a page before/after another zone item of the same page 
355     * @param zoneItemId The identifier of the zone item to move
356     * @param zoneName The destination zone name
357     * @param beforeOrAfter true if before or false after
358     * @param beforeOrAfterItemId The target zone item can be null
359     * @param pageId The concerned page id
360     * @return true when success
361     * @throws UnknownAmetysObjectException If an error occurred
362     * @throws AmetysRepositoryException If an error occurred
363     * @throws RepositoryException If an error occurred
364     */
365    @Callable
366    public boolean moveZoneItemTo(String zoneItemId, String zoneName, boolean beforeOrAfter, String beforeOrAfterItemId, String pageId) throws UnknownAmetysObjectException, AmetysRepositoryException, RepositoryException
367    {
368        UserIdentity user = _currentUserProvider.getUser();
369        
370        ModifiableSitemapElement sitemapElement = _resolver.resolveById(pageId);
371        
372        Zone zone;
373        if (!sitemapElement.hasZone(zoneName))
374        {
375            zone = sitemapElement.createZone(zoneName);
376        }
377        else
378        {
379            zone = sitemapElement.getZone(zoneName);
380        }
381        
382        ZoneItem zoneItem = sitemapElement instanceof JCRAmetysObject ? (ZoneItem) _resolver.resolveById(zoneItemId, ((JCRAmetysObject) sitemapElement).getNode().getSession()) : (ZoneItem) _resolver.resolveById(zoneItemId);
383        
384        _checkZoneItem(sitemapElement, zoneName, zoneItem, user);
385        
386        _checkArguments(zoneItemId, zoneName, pageId, user, sitemapElement, zone, zoneItem);
387        
388        ((ModifiableZoneItem) zoneItem).moveTo(zone, false);
389        
390        // For the moment the zoneItem is at the end
391        if (StringUtils.isNotBlank(beforeOrAfterItemId))
392        {
393            if (beforeOrAfter)
394            {
395                ZoneItem futureFollowingZoneItem = _resolver.resolveById(beforeOrAfterItemId);
396                ((ModifiableZoneItem) zoneItem).orderBefore(futureFollowingZoneItem);
397            }
398            else
399            {
400                boolean isNext = false;
401                for (ZoneItem zi : zone.getZoneItems())
402                {
403                    if (isNext)
404                    {
405                        ((ModifiableZoneItem) zoneItem).orderBefore(zi);
406                        break;
407                    }
408                    else if (beforeOrAfterItemId.equals(zi.getId()))
409                    {
410                        isNext = true;
411                    }
412                }
413            }
414        }
415        
416        ModifiableSitemapElement zoneItemPage = (ModifiableSitemapElement) zoneItem.getZone().getSitemapElement();
417        if (zoneItemPage.needsSave())
418        {
419            zoneItemPage.saveChanges();
420        }
421        
422        Map<String, Object> eventParams = new HashMap<>();
423        eventParams.put(ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement);
424        eventParams.put(ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItemId);
425        eventParams.put(ObservationConstants.ARGS_ZONE_TYPE, zoneItem.getType());
426        _observationManager.notify(new Event(ObservationConstants.EVENT_ZONEITEM_MOVED, user, eventParams));
427        
428        return true;
429    }
430    
431    /**
432     * Check if the zone item can be moved to the given zone of the given page.
433     * @param page the page.
434     * @param zoneName the zone name.
435     * @param zoneItem the zone item to move.
436     * @param user the user
437     */
438    protected void _checkZoneItem(ModifiableSitemapElement page, String zoneName, ZoneItem zoneItem, UserIdentity user)
439    {
440        if (ZoneType.CONTENT.equals(zoneItem.getType()))
441        {
442            String[] contentTypes = zoneItem.getContent().getTypes();
443            
444            Set<String> availableCTypes = _cTypesAssignmentHandler.getAvailableContentTypes(page, zoneName, true);
445            
446            for (String contentType : contentTypes)
447            {
448                if (!availableCTypes.contains(contentType))
449                {
450                    throw new IllegalArgumentException("The user '" + user + " illegally tried to move a content of type '" + contentType + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'.");
451                }
452            }
453        }
454        else if (ZoneType.SERVICE.equals(zoneItem.getType()))
455        {
456            String serviceId = zoneItem.getServiceId();
457            
458            Set<String> services = _serviceAssignmentHandler.getAvailableServices(page, zoneName);
459            if (!services.contains(serviceId))
460            {
461                throw new IllegalArgumentException("The user '" + user + " illegally tried to move a service of id '" + serviceId + "' to the zone '" + zoneName + "' of page '" + page.getId() + "'.");
462            }
463        }
464    }
465    
466    private void _checkArguments(String zoneItemId, String zoneName, String pageId, UserIdentity user, ModifiableSitemapElement page, Zone zone, ZoneItem zoneItem)
467    {
468        if (_rightManager.hasRight(user, "Web_Rights_Page_OrganizeZoneItem", page) != RightResult.RIGHT_ALLOW)
469        {
470            throw new IllegalArgumentException("User '" + user + "' tryed without convinient privileges to move zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !");
471        }
472        else if (zoneItem == null)
473        {
474            throw new IllegalArgumentException("User '" + user + "' tryed to move unexisting zone item '" + zoneItemId + "' to the zone '" + zoneName + "' of the page '" + pageId + "' !");
475        }
476        else if (zone == null)
477        {
478            throw new IllegalArgumentException("User '" + user + "' tryed to move zone item '" + zoneItemId + "' to an unexisting zone '" + zoneName + "' of the page '" + pageId + "' !");
479        }
480        else if (!(zone instanceof ModifiableZone))
481        {
482            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 !");
483        }
484        else if (!(zoneItem instanceof ModifiableZoneItem))
485        {
486            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 !");
487        }
488    }    
489}