001/*
002 *  Copyright 2011 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.page;
017
018import java.io.InputStream;
019import java.io.OutputStream;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Properties;
023import java.util.Set;
024
025import javax.jcr.Node;
026import javax.jcr.RepositoryException;
027import javax.xml.transform.OutputKeys;
028import javax.xml.transform.TransformerFactory;
029import javax.xml.transform.sax.SAXTransformerFactory;
030import javax.xml.transform.sax.TransformerHandler;
031import javax.xml.transform.stream.StreamResult;
032
033import org.apache.avalon.framework.component.Component;
034import org.apache.avalon.framework.logger.AbstractLogEnabled;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.excalibur.xml.sax.SAXParser;
039import org.apache.xml.serializer.OutputPropertiesFactory;
040import org.xml.sax.ContentHandler;
041import org.xml.sax.InputSource;
042
043import org.ametys.cms.content.references.OutgoingReferences;
044import org.ametys.cms.content.references.OutgoingReferencesExtractor;
045import org.ametys.cms.contenttype.ContentType;
046import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
047import org.ametys.cms.contenttype.ContentTypesHelper;
048import org.ametys.cms.contenttype.MetadataDefinition;
049import org.ametys.cms.contenttype.RichTextUpdater;
050import org.ametys.cms.repository.Content;
051import org.ametys.cms.repository.ModifiableContent;
052import org.ametys.cms.repository.WorkflowAwareContent;
053import org.ametys.cms.repository.WorkflowAwareContentHelper;
054import org.ametys.plugins.repository.AmetysObject;
055import org.ametys.plugins.repository.AmetysObjectIterable;
056import org.ametys.plugins.repository.AmetysObjectResolver;
057import org.ametys.plugins.repository.AmetysRepositoryException;
058import org.ametys.plugins.repository.ModifiableAmetysObject;
059import org.ametys.plugins.repository.RepositoryConstants;
060import org.ametys.plugins.repository.TraversableAmetysObject;
061import org.ametys.plugins.repository.UnknownAmetysObjectException;
062import org.ametys.plugins.repository.metadata.CompositeMetadata;
063import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
064import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
065import org.ametys.plugins.repository.metadata.ModifiableRichText;
066import org.ametys.plugins.repository.version.VersionableAmetysObject;
067import org.ametys.plugins.workflow.support.WorkflowProvider;
068import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
069import org.ametys.web.repository.ModifiableSiteAwareAmetysObject;
070import org.ametys.web.repository.content.WebContent;
071import org.ametys.web.repository.page.ZoneItem.ZoneType;
072import org.ametys.web.repository.site.Site;
073import org.ametys.web.repository.sitemap.Sitemap;
074import org.ametys.web.site.CopyUpdaterExtensionPoint;
075
076import com.opensymphony.workflow.spi.Step;
077
078/**
079 * Component for copying site or pages
080 *
081 */
082public class CopySiteComponent extends AbstractLogEnabled implements Component, Serviceable
083{
084    /** Avalon Role */
085    public static final String ROLE = CopySiteComponent.class.getName();
086    
087    private AmetysObjectResolver _resolver;
088    private WorkflowProvider _workflowProvider;
089    private ContentTypeExtensionPoint _cTypeEP;
090    private ContentTypesHelper _contentTypesHelper;
091    
092    private SAXParser _saxParser;
093    private CopyUpdaterExtensionPoint _updaterEP;
094    private OutgoingReferencesExtractor _outgoingReferencesExtractor;
095    
096    @Override
097    public void service(ServiceManager serviceManager) throws ServiceException
098    {
099        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
100        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
101        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
102        _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
103        _saxParser = (SAXParser) serviceManager.lookup(SAXParser.ROLE);
104        _updaterEP = (CopyUpdaterExtensionPoint) serviceManager.lookup(CopyUpdaterExtensionPoint.ROLE);
105        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) serviceManager.lookup(OutgoingReferencesExtractor.ROLE);
106    }
107
108    /**
109     * This methods must be used after calling <code>copyTo</code> on a Page.
110     * Its updates references to ametys objects for metadata of new created pages and contents
111     * @param originalPage the original page
112     * @param createdPage the created page after a copy
113     * @throws AmetysRepositoryException if an error occurs
114     */
115    public void updateReferencesAfterCopy (Page originalPage, Page createdPage) throws AmetysRepositoryException
116    {
117        // Update references to ametys object on metadata
118        _updateReferencesToAmetysObjects (createdPage.getMetadataHolder(), originalPage, createdPage);
119        
120        for (Zone zone : createdPage.getZones())
121        {
122            _updateReferencesToAmetysObjects (zone.getMetadataHolder(), originalPage, createdPage);
123            
124            for (ZoneItem zoneItem : zone.getZoneItems())
125            {
126                _updateReferencesToAmetysObjects (zoneItem.getMetadataHolder(), originalPage, createdPage);
127                
128                if (zoneItem.getType().equals(ZoneType.SERVICE))
129                {
130                    _updateReferencesToAmetysObjects (zoneItem.getServiceParameters(), originalPage, createdPage);
131                }
132                else
133                {
134                    Content content = zoneItem.getContent();
135                    _updateReferencesToAmetysObjects (content.getMetadataHolder(), originalPage, createdPage);
136                }
137            }
138        }
139        
140        // Browse child pages
141        for (Page childPage : createdPage.getChildrenPages())
142        {
143            updateReferencesAfterCopy ((Page) originalPage.getChild(childPage.getName()), childPage);
144        }
145    }
146    
147    /**
148     * This method must be used after calling <code>copyTo</code> on a Site.
149     * Its updates contents and pages after a site copy
150     * @param originalSite the original site
151     * @param createdSite the created site after copy
152     * @throws AmetysRepositoryException if an error occurs
153     */
154    public void updateSiteAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException
155    {
156        updateContentsAfterCopy (originalSite, createdSite);
157        updatePagesAfterCopy (originalSite, createdSite);
158        
159        Set<String> ids = _updaterEP.getExtensionsIds();
160        for (String id : ids)
161        {
162            _updaterEP.getExtension(id).updateSite(originalSite, createdSite);
163        }
164    }
165    
166    /**
167     * This method re-initializes workflow, updates the site name for web content and updates references to ametys objects on metadata after a site copy
168     * @param initialSite the original site
169     * @param createdSite the created site after copy
170     * @throws AmetysRepositoryException if an error occurs 
171     */
172    public void updateContentsAfterCopy (Site initialSite, Site createdSite) throws AmetysRepositoryException
173    {
174        AmetysObjectIterable<Content> contents = createdSite.getContents();
175        for (Content content : contents)
176        {
177            String relPath = content.getPath().substring(createdSite.getPath().length() + 1);
178            WebContent initialContent = initialSite.getChild(relPath);
179            
180            try
181            {
182                // Re-init workflow
183                if (content instanceof WorkflowAwareContent)
184                {
185                    _reinitWorkflow ((WorkflowAwareContent) content);
186                }
187                
188                // Update site name
189                if (content instanceof ModifiableSiteAwareAmetysObject)
190                {
191                    ((ModifiableSiteAwareAmetysObject) content).setSiteName(createdSite.getName());
192                }
193                
194                // Update references to ametys object on metadata
195                _updateReferencesToAmetysObjects (content.getMetadataHolder(), initialSite, createdSite);
196                
197                // Update links in RichText
198                updateLinksInRichText (initialSite, createdSite, initialContent, content);
199                
200                // Updaters
201                Set<String> ids = _updaterEP.getExtensionsIds();
202                for (String id : ids)
203                {
204                    _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, content);
205                }
206                
207                // save
208                if (content instanceof ModifiableAmetysObject)
209                {
210                    ((ModifiableAmetysObject) content).saveChanges();
211                }
212                
213                // Creates the first version
214                if (content instanceof VersionableAmetysObject)
215                {
216                    ((VersionableAmetysObject) content).checkpoint();
217                }
218            }
219            catch (Exception e)
220            {
221                // Do not make the copy fail.
222                getLogger().warn("[Site copy] An error occured while updating content '" + content.getId() + " after copy from initial content '" + initialContent.getId() + "'", e);
223            }
224        }
225        
226        if (createdSite.needsSave())
227        {
228            createdSite.saveChanges();
229        }
230    }
231
232    /**
233     * Updates references all references in a content to another one.
234     * @param initialContent the initial content.
235     * @param destContent the destination content.
236     */
237    public void updateSharedContent(WebContent initialContent, WebContent destContent)
238    {
239        updateSharedContent(initialContent, destContent, true);
240    }
241    
242    /**
243     * Updates references all references in a content to another one.
244     * @param initialContent the initial content.
245     * @param destContent the destination content.
246     * @param reinitWorkflow set to 'true' to reinitialize the workflow
247     */
248    public void updateSharedContent(WebContent initialContent, WebContent destContent, boolean reinitWorkflow)
249    {
250        Site initialSite = initialContent.getSite();
251        Site createdSite = destContent.getSite();
252        
253        // Re-init workflow
254        if (reinitWorkflow && destContent instanceof WorkflowAwareContent)
255        {
256            _reinitWorkflow((WorkflowAwareContent) destContent);
257        }
258        
259        // Update references to ametys object on metadata
260        _updateReferencesToAmetysObjects(destContent.getMetadataHolder(), initialContent, destContent);
261        
262        // Update links in RichText
263        updateLinksInRichText(initialContent, destContent, initialContent, destContent);
264        
265        // Updaters
266        Set<String> ids = _updaterEP.getExtensionsIds();
267        for (String id : ids)
268        {
269            _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, destContent);
270        }
271        
272        // save
273        if (destContent instanceof ModifiableAmetysObject)
274        {
275            ((ModifiableAmetysObject) destContent).saveChanges();
276        }
277        
278        // Creates the first version
279        if (destContent instanceof VersionableAmetysObject)
280        {
281            ((VersionableAmetysObject) destContent).checkpoint();
282        }
283    }
284    
285    /**
286     * This method analyzes content rich texts and update links if necessary
287     * @param initialAO The initial object copied
288     * @param createdAO The target object 
289     * @param initialContent The initial content
290     * @param createdContent The created content after copy to update
291     * @throws AmetysRepositoryException if an error occurs
292     */
293    public void updateLinksInRichText (TraversableAmetysObject initialAO, TraversableAmetysObject createdAO, Content initialContent, Content createdContent) throws AmetysRepositoryException
294    {
295        try
296        {
297            Map<String, Object> params = new HashMap<>();
298            params.put("initialContent", initialContent);
299            params.put("createdContent", createdContent);
300            params.put("initialAO", initialAO);
301            params.put("createdAO", createdAO);
302            
303            CompositeMetadata metadataHolder = createdContent.getMetadataHolder();
304            String[] metadataNames = metadataHolder.getMetadataNames();
305            for (String metadataName : metadataNames)
306            {
307                if (metadataHolder.hasMetadata(metadataName) && metadataHolder.getType(metadataName).equals(MetadataType.RICHTEXT))
308                {
309                    MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(metadataName, createdContent);
310                    
311                    ModifiableRichText richText = (ModifiableRichText) metadataHolder.getRichText(metadataName);
312                    
313                    String referenceContentType = metadataDef.getReferenceContentType();
314                    ContentType contentType = _cTypeEP.getExtension(referenceContentType);
315                    RichTextUpdater richTextUpdater = contentType.getRichTextUpdater();
316                    
317                    if (richTextUpdater != null)
318                    {
319                        try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream())
320                        {
321                            // create a transformer for saving sax into a file
322                            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
323
324                            // create the result where to write
325                            StreamResult result = new StreamResult(os);
326                            th.setResult(result);
327
328                            // create the format of result
329                            Properties format = new Properties();
330                            format.put(OutputKeys.METHOD, "xml");
331                            format.put(OutputKeys.INDENT, "yes");
332                            format.put(OutputKeys.ENCODING, "UTF-8");
333                            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
334                            th.getTransformer().setOutputProperties(format);
335                            
336                            ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params);
337                            _saxParser.parse(new InputSource(is), richTextHandler);
338                        }
339                    }
340                    
341                }
342            }
343            
344            // Outgoing references
345            if (createdContent instanceof ModifiableContent)
346            {
347                Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(createdContent);
348                ((ModifiableContent) createdContent).setOutgoingReferences(outgoingReferencesByPath);
349                
350            }
351        }
352        catch (Exception e)
353        {
354            // Do not failed the copy
355            getLogger().warn("An error occured while updating links in RichText for content '" + createdContent.getId() + " after copy from initial content '" + initialContent.getId() + "'", e);
356        }
357    }
358    
359    /**
360     * This method updates the site name of pages and updates references to ametys objects on page's metadata after a site copy
361     * @param originalSite the original site
362     * @param createdSite the created site after copy
363     * @throws AmetysRepositoryException if an error occurs 
364     */
365    public void updatePagesAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException
366    {
367        AmetysObjectIterable<Sitemap> sitemaps = createdSite.getSitemaps();
368        
369        for (Sitemap sitemap : sitemaps)
370        {
371            for (Page page : sitemap.getChildrenPages())
372            {
373                _updatePageAfterCopy (originalSite, createdSite, page);
374            }
375        }
376    }
377    
378    private void _updatePageAfterCopy (Site originalSite, Site createdSite, Page page) throws AmetysRepositoryException
379    {
380        try
381        {
382            // Update site name
383            if (page instanceof ModifiablePage)
384            {
385                ((ModifiablePage) page).setSiteName(createdSite.getName());
386            }
387            
388            // Update references to ametys object on metadata
389            _updateReferencesToAmetysObjects (page.getMetadataHolder(), originalSite, createdSite);
390            
391            for (Zone zone : page.getZones())
392            {
393                _updateReferencesToAmetysObjects (zone.getMetadataHolder(), originalSite, createdSite);
394                
395                for (ZoneItem zoneItem : zone.getZoneItems())
396                {
397                    _updateReferencesToAmetysObjects (zoneItem.getMetadataHolder(), originalSite, createdSite);
398                    
399                    if (zoneItem.getType().equals(ZoneType.SERVICE))
400                    {
401                        _updateReferencesToAmetysObjects (zoneItem.getServiceParameters(), originalSite, createdSite);
402                    }
403                    else if (zoneItem.getType().equals(ZoneType.CONTENT) && zoneItem instanceof ModifiableZoneItem)
404                    {
405                        Content content = zoneItem.getContent();
406                        String path = content.getPath();
407                        
408                        String originalPath = originalSite.getPath();
409                        if (path.startsWith(originalPath))
410                        {
411                            String relPath = path.substring(originalPath.length() + 1);
412                            try
413                            {
414                                // Find symmetric object on copied sub-tree
415                                Content child = createdSite.getChild(relPath);
416                                ((ModifiableZoneItem) zoneItem).setContent(child);
417                            }
418                            catch (UnknownAmetysObjectException e)
419                            {
420                                // Nothing
421                            }
422                        }
423                    }
424                }
425            }
426        }
427        catch (AmetysRepositoryException e)
428        {
429            // Do not failed the copy
430            getLogger().warn("An error occured while updating page '" + page.getId() + "' (" + page.getPathInSitemap() + ") after copy", e);
431        }
432        
433        // Browse child pages
434        for (Page childPage : page.getChildrenPages())
435        {
436            _updatePageAfterCopy (originalSite, createdSite, childPage);
437        }
438        
439        // Updaters
440        Set<String> ids = _updaterEP.getExtensionsIds();
441        for (String id : ids)
442        {
443            _updaterEP.getExtension(id).updatePage(originalSite, createdSite, page);
444        }
445    }
446    
447    private void _updateReferencesToAmetysObjects (CompositeMetadata metadataHolder, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO)
448    {
449        String[] metadataNames = metadataHolder.getMetadataNames();
450        for (String metadataName : metadataNames)
451        {
452            if (MetadataType.COMPOSITE.equals(metadataHolder.getType(metadataName)))
453            {
454                _updateReferencesToAmetysObjects (metadataHolder.getCompositeMetadata(metadataName), originalAO, createdAO);
455            }
456            else if (_isAmetysObject(metadataHolder, metadataName))
457            {
458                _updateReferenceToAmetysObject (metadataHolder, metadataName, originalAO, createdAO);
459            }
460        }
461    }
462    
463    private void _updateReferenceToAmetysObject (CompositeMetadata metadataHolder, String metadataName, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO) throws AmetysRepositoryException
464    {
465        if (metadataHolder instanceof ModifiableCompositeMetadata)
466        {
467            String id = metadataHolder.getString(metadataName);
468            
469            AmetysObject ametysObject = _resolver.resolveById(id);
470            String path = ametysObject.getPath();
471            
472            String originalPath = originalAO.getPath();
473            if (path.startsWith(originalPath + "/"))
474            {
475                String relPath = path.substring(originalPath.length() + 1);
476                try
477                {
478                    // Find symmetric object on copied sub-tree
479                    AmetysObject child = createdAO.getChild(relPath);
480                    ((ModifiableCompositeMetadata) metadataHolder).setMetadata(metadataName, child.getId());
481                }
482                catch (UnknownAmetysObjectException e)
483                {
484                    getLogger().warn("Object of path " + relPath + " was not found on copied sub-tree " + createdAO.getPath(), e);
485                }
486                catch (AmetysRepositoryException e)
487                {
488                    getLogger().error("Unable to retrieve object of path " + relPath + " on copied sub-tree " + createdAO.getPath(), e);
489                }
490            }
491        }
492    }
493    
494    private boolean _isAmetysObject (CompositeMetadata metadataHolder, String metadataName)
495    {
496        try
497        {
498            if (metadataHolder.getType(metadataName).equals(MetadataType.STRING))
499            {
500                if (metadataHolder.isMultiple(metadataName))
501                {
502                    String[] values = metadataHolder.getStringArray(metadataName);
503                    for (String value : values)
504                    {
505                        if (_resolver.hasAmetysObjectForId(value))
506                        {
507                            return true;
508                        }
509                    }
510                }
511                else
512                {
513                    String value = metadataHolder.getString(metadataName);
514                    if (_resolver.hasAmetysObjectForId(value))
515                    {
516                        return true;
517                    }
518                }
519            }
520            
521            return false;
522        }
523        catch (AmetysRepositoryException e)
524        {
525            return false;
526        }
527    }
528    
529    private void _reinitWorkflow (WorkflowAwareContent content) throws AmetysRepositoryException
530    {
531        try
532        {
533            long wId = content.getWorkflowId();
534            
535            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
536            String workflowName = workflow.getWorkflowName(wId);
537            
538            // 1 - Delete the cloned workflow
539            WorkflowAwareContentHelper.removeWorkflowId(content);
540            
541            // For legacy purpose, delete the workflow reference property if exists (only for contents created on 3.x versions)
542            Node node = content.getNode();
543            try
544            {
545                if (node.hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef"))
546                {
547                    node.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef").remove();
548                }
549            }
550            catch (RepositoryException e)
551            {
552                throw new AmetysRepositoryException("Unable to remove workflowId property", e);
553            }
554            
555            workflow.removeWorkflow(wId);
556            
557            // 2 - Initialize new workflow instance
558            HashMap<String, Object> inputs = new HashMap<>();
559            long workflowId = workflow.initialize(workflowName, 0, inputs);
560            content.setWorkflowId(workflowId);
561            
562            // Update current step property
563            Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next();
564            content.setCurrentStepId(currentStep.getStepId());
565        }
566        catch (Exception e)
567        {
568            throw new AmetysRepositoryException("Unable to initialize workflow for content " + content.getId(), e);
569        }
570        
571    }
572}