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