001/*
002 *  Copyright 2012 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.content.shared;
017
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Map;
024import java.util.Set;
025
026import javax.jcr.Node;
027import javax.jcr.PropertyIterator;
028import javax.jcr.RepositoryException;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.logger.AbstractLogEnabled;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.commons.lang.ArrayUtils;
036import org.apache.commons.lang.StringUtils;
037
038import org.ametys.cms.CmsConstants;
039import org.ametys.cms.FilterNameHelper;
040import org.ametys.cms.ObservationConstants;
041import org.ametys.cms.repository.Content;
042import org.ametys.cms.repository.DefaultContent;
043import org.ametys.core.observation.Event;
044import org.ametys.core.observation.ObservationManager;
045import org.ametys.core.user.CurrentUserProvider;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
048import org.ametys.plugins.explorer.resources.ResourceCollection;
049import org.ametys.plugins.repository.AmetysObject;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.CopiableAmetysObject;
053import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
054import org.ametys.plugins.repository.RemovableAmetysObject;
055import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
056import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
057import org.ametys.plugins.repository.jcr.JCRAmetysObject;
058import org.ametys.web.filter.SharedContentsHelper;
059import org.ametys.web.repository.content.ModifiableWebContent;
060import org.ametys.web.repository.content.SharedContent;
061import org.ametys.web.repository.content.WebContent;
062import org.ametys.web.repository.content.jcr.DefaultSharedContent;
063import org.ametys.web.repository.content.jcr.DefaultSharedContentFactory;
064import org.ametys.web.repository.content.jcr.DefaultWebContent;
065import org.ametys.web.repository.page.CopySiteComponent;
066import org.ametys.web.repository.page.ModifiableZoneItem;
067import org.ametys.web.repository.page.Page;
068import org.ametys.web.repository.page.ZoneItem;
069import org.ametys.web.repository.page.ZoneItem.ZoneType;
070import org.ametys.web.repository.site.Site;
071
072/**
073 * Component which provides methods to manage shared contents (creation, validation, and so on).
074 */
075public class SharedContentManager extends AbstractLogEnabled implements Serviceable, Component
076{
077    
078    /** The avalon role. */
079    public static final String ROLE = SharedContentManager.class.getName();
080    
081    /** The ametys object resolver. */
082    protected AmetysObjectResolver _resolver;
083    
084    /** The observation manager. */
085    protected ObservationManager _observationManager;
086    
087    /** The current user provider. */
088    protected CurrentUserProvider _currentUserProvider;
089    
090    /** The site copy component. */
091    protected CopySiteComponent _copySiteComponent;
092    
093    @Override
094    public void service(ServiceManager serviceManager) throws ServiceException
095    {
096        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
097        _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
098        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
099        _copySiteComponent = (CopySiteComponent) serviceManager.lookup(CopySiteComponent.ROLE);
100    }
101    
102    /**
103     * Create a {@link SharedContent} from an original content.
104     * @param site the site in which to create the shared content.
105     * @param originalContent the original content.
106     * @return the created shared content.
107     */
108    public DefaultSharedContent createSharedContent(Site site, DefaultContent originalContent)
109    {
110        try
111        {
112            ModifiableTraversableAmetysObject contentRoot = site.getRootContents();
113            
114            // Get a reference on the original node.
115            Node originalNode = originalContent.getNode();
116            
117            String copyName = originalContent.getName() + "-shared";
118            
119            DefaultSharedContent content = createContent(copyName, contentRoot);
120            
121            // Store the reference to the original content.
122            content.getNode().setProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY, originalNode);
123            
124            String originalLanguage = originalContent.getLanguage();
125            if (originalLanguage != null)
126            {
127                content.setLanguage(originalContent.getLanguage());
128            }
129            
130            // Copy standard properties.
131            content.setTypes(originalContent.getTypes());
132            // Copy title needs model -> types must to be set before
133            SharedContentsHelper.copyTitle(originalContent, content);
134            
135            content.setCreator(originalContent.getCreator());
136            content.setCreationDate(originalContent.getCreationDate());
137            content.setLastContributor(originalContent.getLastContributor());
138            content.setLastModified(originalContent.getLastModified());
139            
140            content.saveChanges();
141            
142            // Copy the content data.
143            copyContentData(originalContent, content);
144            
145            // Create the first version
146            content.checkpoint();
147            
148            // Validate the shared content if the original content is validated.
149            if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL))
150            {
151                validateContent(content);
152            }
153            
154            return content;
155        }
156        catch (RepositoryException e)
157        {
158            throw new AmetysRepositoryException(e);
159        }
160    }
161    
162    /**
163     * Copy the data of a content into a shared content.
164     * @param originalContent the content to copy data from.
165     * @param content the content to copy data to.
166     * @throws AmetysRepositoryException if an error occurs during copy
167     */
168    public void copyContentData(DefaultContent originalContent, DefaultSharedContent content) throws AmetysRepositoryException
169    {
170        String currentRevision = originalContent.getRevision();
171        
172        try
173        {
174            if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL))
175            {
176                // Switch the content to its live revision.
177                originalContent.switchToLabel(CmsConstants.LIVE_LABEL);
178                
179                // Copy metadata.
180                removeAllData(content.getDataHolder());
181                originalContent.copyTo(content.getDataHolder());
182                
183                // Copy unversioned metadata.
184                removeAllData(content.getUnversionedDataHolder());
185                originalContent.getUnversionedDataHolder().copyTo(content.getUnversionedDataHolder());
186                
187                // Copy attachments.
188                if (originalContent instanceof WebContent)
189                {
190                    WebContent originalWebContent = (WebContent) originalContent;
191                    ResourceCollection originalRootAttachments = originalWebContent.getRootAttachments();
192                    if (originalRootAttachments != null && originalRootAttachments instanceof CopiableAmetysObject)
193                    {
194                        // Remove the attachment root before copying.
195                        ModifiableResourceCollection rootAttachments = (ModifiableResourceCollection) content.getRootAttachments();
196                        if (rootAttachments != null) // root attachments can be missing if the content is an (unmodifiable) old version
197                        {
198                            rootAttachments.remove();
199                            ((CopiableAmetysObject) originalRootAttachments).copyTo(content, originalRootAttachments.getName()); 
200                        }
201                        
202                    }
203                    
204                    _copySiteComponent.updateSharedContent(originalWebContent, content);
205                }
206                
207                // The "site" metadata has been copied: revert it to the real value.
208                content.setSiteName(content.getSite().getName());
209                
210                content.saveChanges();
211            }
212        }
213        finally
214        {
215            originalContent.switchToRevision(currentRevision);
216        }
217    }
218    
219    /**
220     * Validate a shared content.
221     * @param content the content to validate.
222     */
223    public void validateContent(DefaultSharedContent content)
224    {
225        internalValidateContent(content);
226        
227        UserIdentity user = _currentUserProvider.getUser();
228        
229        // Notify observers that the content has been validated.
230        Map<String, Object> eventParams = new HashMap<>();
231        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
232        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
233        
234        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_VALIDATED, user, eventParams));
235        
236    }
237    
238    /**
239     * Invalidate a shared content.
240     * @param content the content to invalidate.
241     */
242    public void invalidateSharedContent(DefaultSharedContent content)
243    {
244        internalUnpublishContent(content);
245        
246        UserIdentity user = _currentUserProvider.getUser();
247        
248        // Notify observers that the content has been unpublished.
249        Map<String, Object> eventParams = new HashMap<>();
250        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
251        eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, content.getSiteName());
252        
253        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_UNTAG_LIVE, user, eventParams));
254    }
255    
256    /**
257     * Test if there are shared contents created from the given content.
258     * @param content the content to test.
259     * @return true if at least one shared content was created from the given content, false otherwise.
260     */
261    public boolean hasSharedContents(Content content)
262    {
263        try
264        {
265            if (content instanceof DefaultContent)
266            {
267                DefaultContent originalContent = (DefaultContent) content;
268                PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY);
269                
270                return references.hasNext();
271            }
272            
273            return false;
274        }
275        catch (RepositoryException e)
276        {
277            throw new AmetysRepositoryException(e);
278        }
279    }
280    
281    /**
282     * Get the list of shared contents created from the given content.
283     * @param content the content of which to get referencing shared contents.
284     * @return the shared contents created from the given content.
285     */
286    public Set<SharedContent> getSharedContents(Content content)
287    {
288        try
289        {
290            Set<SharedContent> sharedContents = new HashSet<>();
291            
292            if (content instanceof DefaultContent)
293            {
294                // Resolve the content to switch its revision without impacting the original object.
295                DefaultContent originalContent = (DefaultContent) content;
296                PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY);
297                while (references.hasNext())
298                {
299                    Node referer = references.nextProperty().getParent();
300                    DefaultSharedContent sharedContent = _resolver.resolve(referer, true);
301                    if (sharedContent != null)
302                    {
303                        sharedContents.add(sharedContent);
304                    }
305                }
306            }
307            
308            return sharedContents;
309        }
310        catch (RepositoryException e)
311        {
312            throw new AmetysRepositoryException(e);
313        }
314    }
315    
316    /**
317     * Remove the list of shared contents created from the given content.
318     * @param content the content of which to remove referencing shared content references.
319     */
320    public void removeSharedContentReferences(Content content)
321    {
322        try
323        {
324            // Get shared contents which reference the content.
325            Set<SharedContent> sharedContents = getSharedContents(content);
326            for (SharedContent sharedContent : sharedContents)
327            {
328                if (sharedContent instanceof JCRAmetysObject)
329                {
330                    Node node = ((JCRAmetysObject) sharedContent).getNode();
331                    node.getProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY).remove();
332                }
333            }
334        }
335        catch (RepositoryException e)
336        {
337            throw new AmetysRepositoryException(e);
338        }
339    }
340    
341    /**
342     * Switch all shared contents created from the given content into default contents
343     * @param content the initial content with shared content references.
344     */
345    public void switchSharedContentReferences (Content content)
346    {
347        // Get shared contents which reference the content.
348        Set<SharedContent> sharedContents = getSharedContents(content);
349        
350        // Store migrated contents per site
351        Map<String, Content> migratedContents = new HashMap<>();
352        
353        for (SharedContent sharedContent : sharedContents)
354        {
355            String siteName = sharedContent.getSiteName();
356            String sharedContentName = sharedContent.getName();
357            String sharedContentId = sharedContent.getId();
358            String contentName = StringUtils.removeEnd(sharedContentName, "-shared");
359            
360            ModifiableTraversableAmetysObject parent = sharedContent.getParent();
361            
362            // Prepare deletion of shared content
363            Map<String, Object> sharedEventParams = new HashMap<>();
364            sharedEventParams.put(ObservationConstants.ARGS_CONTENT, sharedContent);
365            sharedEventParams.put(ObservationConstants.ARGS_CONTENT_NAME, sharedContentName);
366            sharedEventParams.put(ObservationConstants.ARGS_CONTENT_ID, sharedContentId);
367            sharedEventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, siteName);
368            _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), sharedEventParams));
369            
370            if (content instanceof DefaultWebContent)
371            {
372                Content cContent = migratedContents.get(siteName);
373                
374                if (cContent == null)
375                {
376                    // Do a full copy of initial content - only one per site
377                    cContent = ((DefaultWebContent) content).copyTo(parent, contentName);
378                    if (cContent instanceof ModifiableWebContent)
379                    {
380                        ((ModifiableWebContent) cContent).setSiteName(siteName);
381                        _copySiteComponent.updateSharedContent((DefaultWebContent) content, (ModifiableWebContent) cContent, false);
382                    }
383                }
384                        
385                // Update referenced zone items 
386                Set<Page> refPages = new HashSet<>();
387                Collection<ZoneItem> refZoneItems = sharedContent.getReferencingZoneItems();
388                for (ZoneItem zoneItem : refZoneItems)
389                {
390                    Page page = zoneItem.getZone().getPage();
391                    
392                    if (zoneItem instanceof ModifiableZoneItem)
393                    {
394                        // Update the zone item with the copied content
395                        ((ModifiableZoneItem) zoneItem).setContent(cContent);
396                        
397                        Map<String, Object> eventParams = new HashMap<>();
398                        eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
399                        eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM, zoneItem);
400                        eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId());
401                        eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT);
402                        
403                        _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_ZONEITEM_MODIFIED, _currentUserProvider.getUser(), eventParams));
404                    }
405                    
406                    refPages.add(page);
407                }
408                
409                for (Page page : refPages)
410                {
411                    Map<String, Object> eventParams = new HashMap<>();
412                    eventParams.put(ObservationConstants.ARGS_CONTENT, cContent);
413                    eventParams.put(ObservationConstants.ARGS_CONTENT_ID, cContent.getId());
414                    eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page);
415                    
416                    String eventId = migratedContents.containsKey(siteName) ? ObservationConstants.EVENT_CONTENT_MODIFIED : ObservationConstants.EVENT_CONTENT_ADDED;
417                    _observationManager.notify(new Event(eventId, _currentUserProvider.getUser(), eventParams));
418                }
419                
420                migratedContents.put(siteName, cContent);
421            }
422            
423            // Remove the shared content
424            sharedContent.remove();
425            parent.saveChanges();
426            
427            _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), sharedEventParams));
428        }
429    }
430    
431    /**
432     * Create a shared content in the given contents root.
433     * @param desiredContentName the desired content name.
434     * @param contentsNode the contents root.
435     * @return the created content.
436     */
437    protected DefaultSharedContent createContent(String desiredContentName, ModifiableTraversableAmetysObject contentsNode)
438    {
439        DefaultSharedContent content = null;
440        
441        String contentName = FilterNameHelper.filterName(desiredContentName);
442        int errorCount = 0;
443        do
444        {
445            if (errorCount != 0)
446            {
447                contentName = FilterNameHelper.filterName(desiredContentName + " " + (errorCount + 1));
448            }
449            try
450            {
451                content = contentsNode.createChild(contentName, DefaultSharedContentFactory.SHARED_CONTENT_NODETYPE);
452            }
453            catch (RepositoryIntegrityViolationException e)
454            {
455                // Content name is already used
456                errorCount++;
457            }
458        }
459        while (content == null);
460        
461        return content;
462    }
463    
464    /**
465     * Validate a shared content.
466     * @param content the content to validate.
467     */
468    protected void internalValidateContent(DefaultSharedContent content)
469    {
470        Date validationDate = new Date();
471        
472        boolean isValid = Arrays.asList(content.getAllLabels()).contains(CmsConstants.LIVE_LABEL);
473        if (!isValid)
474        {
475            content.setLastMajorValidationDate(validationDate);
476        }
477        
478        content.setLastValidationDate(validationDate);
479        if (content.getFirstValidationDate() == null)
480        {
481            content.setFirstValidationDate(validationDate);
482        }
483        
484        content.saveChanges();
485        
486        content.checkpoint();
487        content.addLabel(CmsConstants.LIVE_LABEL, true);
488    }
489    
490    /**
491     * Unpublish a shared content.
492     * @param content the content to unpublish.
493     */
494    protected void internalUnpublishContent(DefaultSharedContent content)
495    {
496        if (ArrayUtils.contains(content.getAllLabels(), CmsConstants.LIVE_LABEL))
497        {
498            content.removeLabel(CmsConstants.LIVE_LABEL);
499        }
500        
501        content.saveChanges();
502    }
503    
504    /**
505     * Remove all children of a {@link ModifiableTraversableAmetysObject}.
506     * @param rootObject the traversable ametys object to empty.
507     */
508    protected void removeAllChildren(ModifiableTraversableAmetysObject rootObject)
509    {
510        for (AmetysObject object : rootObject.getChildren())
511        {
512            if (object instanceof RemovableAmetysObject)
513            {
514                ((RemovableAmetysObject) object).remove();                
515            }
516        }
517        
518        rootObject.saveChanges();
519    }
520    
521    /**
522     * Remove all data of a data holder.
523     * @param dataHolder the data holder to empty.
524     */
525    protected void removeAllData(ModifiableDataHolder dataHolder)
526    {
527        for (String dataName : dataHolder.getDataNames())
528        {
529            dataHolder.removeValue(dataName);
530        }
531    }
532    
533}