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