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