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