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