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