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