001/*
002 *  Copyright 2020 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.plugins.contentio.archive;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.Map;
021import java.util.Objects;
022import java.util.concurrent.ConcurrentHashMap;
023
024import javax.jcr.ItemNotFoundException;
025import javax.jcr.Node;
026import javax.jcr.RepositoryException;
027import javax.jcr.Session;
028import javax.xml.transform.Templates;
029import javax.xml.transform.TransformerConfigurationException;
030import javax.xml.transform.TransformerException;
031import javax.xml.transform.sax.SAXResult;
032import javax.xml.transform.sax.SAXTransformerFactory;
033import javax.xml.transform.sax.TransformerHandler;
034import javax.xml.transform.stream.StreamSource;
035
036import org.apache.cocoon.components.xslt.TraxErrorListener;
037import org.apache.cocoon.util.log.SLF4JLoggerAdapter;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.excalibur.source.Source;
040import org.apache.jackrabbit.commons.xml.ParsingContentHandler;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043import org.xml.sax.ContentHandler;
044import org.xml.sax.SAXException;
045
046import org.ametys.plugins.repository.AmetysRepositoryException;
047
048/**
049 * For {@link Mergers#IGNORE}, we need to use session.getImportContentHandler and filter the SAX events from the XML input stream
050 * <br>For all &lt;sv:property sv:name="jcr:uuid" sv:type="String"&gt; and get their &lt;sv:value&gt;,
051 * query the repository to check if it already exists and if it should be ignored
052 * <br>And for each sax event of sv:node
053 * <ol>
054 * <li>if exists in the repository =&gt; ignore the node import</li>
055 * <li>if does not exist in the repository =&gt; send SAX event</li>
056 * </ol>
057 */
058public final class IgnoreMergerHelper
059{
060    private static final Logger __LOGGER = LoggerFactory.getLogger(IgnoreMergerHelper.class.getPackageName());
061    private static final org.apache.avalon.framework.logger.Logger __AVALON_LOGGER = new SLF4JLoggerAdapter(__LOGGER);
062    
063    // a way to pass the Session object to the helper method #uuidIsNotPresent, which called by the XSL
064    private static Map<Thread, Session> __SESSIONS_BY_REQUEST = new ConcurrentHashMap<>();
065    
066    private IgnoreMergerHelper()
067    {  }
068    
069    /**
070     * Tells if the given node UUID is not present in the reposiotry.
071     * @param nodeUuid The node UUID to test
072     * @return <code>true</code> if no node exists in the repository with this UUID. <code>false</code> if a node exists in the repository with this UUID.
073     */
074    public static boolean uuidIsNotPresent(String nodeUuid)
075    {
076        if (StringUtils.isBlank(nodeUuid))
077        {
078            return true;
079        }
080        
081        try
082        {
083            Session session = __SESSIONS_BY_REQUEST.get(Thread.currentThread());
084            Node node = session.getNodeByIdentifier(nodeUuid);
085            Objects.requireNonNull(node, "Node cannot be null.");
086            __LOGGER.debug("IgnoreMergerHelper#uuidIsNotPresent is called for argument '{}'. A node with this uuid was found. Returned result is false.", nodeUuid);
087            return false;
088        }
089        catch (ItemNotFoundException e)
090        {
091            __LOGGER.debug("IgnoreMergerHelper#uuidIsNotPresent is called for argument '{}'. No node was found with this uuid. Returned result is true.", nodeUuid);
092            return true;
093        }
094        catch (RepositoryException e)
095        {
096            throw new AmetysRepositoryException(e);
097        }
098    }
099    
100    static void jcrImportXml(Session session, String parentAbsPath, InputStream in, int importUuidBehavior, Mergers mergers) throws RepositoryException, IOException
101    {
102        ContentHandler importContentHandler = session.getImportContentHandler(parentAbsPath, importUuidBehavior);
103        Thread currentThread = Thread.currentThread();
104        try
105        {
106            __SESSIONS_BY_REQUEST.put(currentThread, session);
107            TransformerHandler filteringContentHandler = _sendFilteredSaxEvents(importContentHandler, mergers);
108            _sendSaxEventsFromInputStream(in, filteringContentHandler);
109        }
110        catch (TransformerException | SAXException e)
111        {
112            throw new IOException(e);
113        }
114        finally
115        {
116            __SESSIONS_BY_REQUEST.remove(currentThread);
117        }
118    }
119    
120    private static TransformerHandler _sendFilteredSaxEvents(ContentHandler receiverContentHandler, Mergers mergers) throws IOException, TransformerConfigurationException
121    {
122        // Filter thanks to a XSL
123        Source xsltSource = null;
124        try
125        {
126            xsltSource = mergers._sourceResolver.resolveURI("plugin:contentio://stylesheets/archive/ignore-merger-helper.xsl");
127            try (InputStream inputStream = xsltSource.getInputStream())
128            {
129                StreamSource xsltStreamSource = new StreamSource(inputStream);
130                
131                SAXTransformerFactory saxTransformerFactory = Archivers.getSaxTransformerFactory();
132                Templates xsltTemplates = saxTransformerFactory.newTemplates(xsltStreamSource);
133                TransformerHandler filteringTransformerHandler = saxTransformerFactory.newTransformerHandler(xsltTemplates);
134                TraxErrorListener errorListener = new TraxErrorListener(__AVALON_LOGGER, xsltSource.getURI());
135                filteringTransformerHandler.getTransformer().setErrorListener(errorListener);
136                
137                filteringTransformerHandler.setResult(new SAXResult(receiverContentHandler));
138                
139                return filteringTransformerHandler;
140            }
141        }
142        finally
143        {
144            if (xsltSource != null)
145            {
146                mergers._sourceResolver.release(xsltSource);
147            }
148        }
149    }
150    
151    private static void _sendSaxEventsFromInputStream(InputStream in, ContentHandler wrappedContentHandler) throws SAXException, IOException
152    {
153        var parsingContentHandler = new ParsingContentHandler(wrappedContentHandler);
154        parsingContentHandler.parse(in);
155    }
156}