001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    https://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.api.dsmlv2;
021
022
023import java.nio.charset.StandardCharsets;
024import java.util.Arrays;
025import java.util.Base64;
026import java.util.Collection;
027
028import javax.xml.transform.Transformer;
029import javax.xml.transform.TransformerConfigurationException;
030import javax.xml.transform.TransformerException;
031import javax.xml.transform.TransformerFactory;
032import javax.xml.transform.stream.StreamSource;
033
034import org.apache.directory.api.asn1.util.Asn1Buffer;
035import org.apache.directory.api.dsmlv2.actions.ReadSoapHeader;
036import org.apache.directory.api.dsmlv2.request.BatchRequestDsml;
037import org.apache.directory.api.dsmlv2.request.BatchRequestDsml.Processing;
038import org.apache.directory.api.dsmlv2.request.BatchRequestDsml.ResponseOrder;
039import org.apache.directory.api.i18n.I18n;
040import org.apache.directory.api.ldap.codec.api.LdapApiService;
041import org.apache.directory.api.ldap.model.message.Control;
042import org.apache.directory.api.util.Strings;
043import org.dom4j.Document;
044import org.dom4j.Element;
045import org.dom4j.Namespace;
046import org.dom4j.QName;
047import org.dom4j.io.DocumentResult;
048import org.dom4j.io.DocumentSource;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051import org.xmlpull.v1.XmlPullParser;
052import org.xmlpull.v1.XmlPullParserException;
053
054
055/**
056 * This class is a Helper class for the DSML Parser
057 *
058 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
059 */
060public final class ParserUtils
061{
062    /** W3C XML Schema URI. */
063    public static final String XML_SCHEMA_URI = "http://www.w3.org/2001/XMLSchema";
064
065    /** W3C XML Schema Instance URI. */
066    public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance";
067
068    /** Base-64 identifier. */
069    public static final String BASE64BINARY = "base64Binary";
070
071    /** XSI namespace prefix. */
072    public static final String XSI = "xsi";
073
074    /** XSD namespace prefix. */
075    public static final String XSD = "xsd";
076
077    /** XSD namespace prefix with ':'. */
078    public static final String XSD_COLON = "xsd:";
079
080    /** The DSML namespace */
081    public static final Namespace DSML_NAMESPACE = new Namespace( null, "urn:oasis:names:tc:DSML:2:0:core" );
082
083    /** The XSD namespace */
084    public static final Namespace XSD_NAMESPACE = new Namespace( XSD, XML_SCHEMA_URI );
085
086    /** The XSI namespace */
087    public static final Namespace XSI_NAMESPACE = new Namespace( XSI, XML_SCHEMA_INSTANCE_URI );
088
089    /** A logger for this class */
090    private static final Logger LOG = LoggerFactory.getLogger( ParserUtils.class );
091
092    /**
093     * GrammarAction that reads the SOAP header data
094     */
095    public static final GrammarAction READ_SOAP_HEADER = new ReadSoapHeader();
096
097    private ParserUtils()
098    {
099    }
100
101
102    /**
103     * Returns the value of the attribute 'type' of the "XMLSchema-instance' namespace if it exists
104     *
105     * @param xpp the XPP parser to use
106     * @return the value of the attribute 'type' of the "XMLSchema-instance' namespace if it exists
107     */
108    public static String getXsiTypeAttributeValue( XmlPullParser xpp )
109    {
110        String type = null;
111        int nbAttributes = xpp.getAttributeCount();
112
113        for ( int i = 0; i < nbAttributes; i++ )
114        {
115            // Checking if the attribute 'type' from XML Schema Instance namespace is used.
116            if ( DsmlLiterals.TYPE.equals( xpp.getAttributeName( i ) )
117                && xpp.getNamespace( xpp.getAttributePrefix( i ) ).equals( XML_SCHEMA_INSTANCE_URI ) )
118            {
119                type = xpp.getAttributeValue( i );
120                break;
121            }
122        }
123
124        return type;
125    }
126
127
128    /**
129     * Tells is the given value is a Base64 binary value
130     *
131     * @param parser the XPP parser to use
132     * @param attrValue the attribute value
133     * @return true if the value of the current tag is Base64BinaryEncoded, false if not
134     */
135    public static boolean isBase64BinaryValue( XmlPullParser parser, String attrValue )
136    {
137        if ( attrValue == null )
138        {
139            return false;
140        }
141
142        // We are looking for something that should look like that: "aNameSpace:base64Binary"
143        // We split the String. The first element should be the namespace prefix and the second "base64Binary"
144        String[] splitedString = attrValue.split( ":" );
145
146        return ( splitedString.length == 2 ) && ( XML_SCHEMA_URI.equals( parser.getNamespace( splitedString[0] ) ) )
147            && ( BASE64BINARY.equals( splitedString[1] ) );
148    }
149
150
151    /**
152     * Indicates if the value needs to be encoded as Base64
153     *
154     * @param value the value to check
155     * @return true if the value needs to be encoded as Base64
156     */
157    public static boolean needsBase64Encoding( Object value )
158    {
159        return ( value instanceof byte[] );
160    }
161
162
163    /**
164     * Encodes the value as a Base64 String
165     *
166     * @param value the value to encode
167     * @return the value encoded as a Base64 String
168     */
169    public static String base64Encode( Object value )
170    {
171        if ( value instanceof byte[] )
172        {
173            return new String( Base64.getEncoder().encode( ( byte[] ) value ), StandardCharsets.UTF_8 );
174        }
175        else if ( value instanceof String )
176        {
177            return new String( Base64.getEncoder().encode( Strings.getBytesUtf8( ( String ) value ) ), StandardCharsets.UTF_8 );
178        }
179
180        return Strings.EMPTY_STRING;
181    }
182
183
184    /**
185     * Parses and verify the parsed value of the requestID
186     *
187     * @param attributeValue the value of the attribute
188     * @param xpp the XmlPullParser
189     * @return the int value of the resquestID
190     * @throws XmlPullParserException if RequestID isn't an Integer and if requestID is below 0
191     */
192    public static int parseAndVerifyRequestID( String attributeValue, XmlPullParser xpp ) throws XmlPullParserException
193    {
194        try
195        {
196            int requestID = Integer.parseInt( attributeValue );
197
198            if ( requestID < 0 )
199            {
200                throw new XmlPullParserException( I18n.err( I18n.ERR_03016_BELOW_0_REQUEST_ID, requestID ), xpp, null );
201            }
202
203            return requestID;
204        }
205        catch ( NumberFormatException nfe )
206        {
207            throw new XmlPullParserException( I18n.err( I18n.ERR_03012_REQUEST_ID_NOT_INTEGER ), xpp, nfe );
208        }
209    }
210
211
212    /**
213     * Adds Controls to the given Element.
214     *
215     * @param codec The LDAP Service to use
216     * @param element the element to add the Controls to
217     * @param controls a List of Controls
218     * @param isRequest A flag set to <tt>true</tt> if teh LDapMessage is a request
219     */
220    public static void addControls( LdapApiService codec, Element element, Collection<Control> controls, boolean isRequest )
221    {
222        if ( controls != null )
223        {
224            for ( Control control : controls )
225            {
226                Element controlElement = element.addElement( DsmlLiterals.CONTROL );
227
228                if ( control.getOid() != null )
229                {
230                    controlElement.addAttribute( DsmlLiterals.TYPE, control.getOid() );
231                }
232
233                if ( control.isCritical() )
234                {
235                    controlElement.addAttribute( DsmlLiterals.CRITICALITY, DsmlLiterals.TRUE );
236                }
237
238                Asn1Buffer asn1Buffer = new Asn1Buffer();
239
240                if ( isRequest )
241                {
242                    codec.getRequestControlFactories().get( control.getOid() ).encodeValue( asn1Buffer, control );
243                }
244                else
245                {
246                    codec.getResponseControlFactories().get( control.getOid() ).encodeValue( asn1Buffer, control );
247                }
248                
249               byte[] value = asn1Buffer.getBytes().array();
250
251                if ( value != null )
252                {
253                    if ( ParserUtils.needsBase64Encoding( value ) )
254                    {
255                        element.getDocument().getRootElement().add( XSD_NAMESPACE );
256                        element.getDocument().getRootElement().add( XSI_NAMESPACE );
257
258                        Element valueElement = controlElement.addElement( DsmlLiterals.CONTROL_VALUE ).addText(
259                            ParserUtils.base64Encode( value ) );
260                        valueElement.addAttribute( new QName( DsmlLiterals.TYPE, XSI_NAMESPACE ), ParserUtils.XSD_COLON
261                            + ParserUtils.BASE64BINARY );
262                    }
263                    else
264                    {
265                        controlElement.addElement( DsmlLiterals.CONTROL_VALUE ).setText( Arrays.toString( value ) );
266                    }
267                }
268            }
269        }
270    }
271
272
273    /**
274     * Indicates if a request ID is needed.
275     *
276     * @param container the associated container
277     * @return true if a request ID is needed (ie Processing=Parallel and ResponseOrder=Unordered)
278     * @throws XmlPullParserException if the batch request has not been parsed yet
279     */
280    public static boolean isRequestIdNeeded( Dsmlv2Container container ) throws XmlPullParserException
281    {
282        BatchRequestDsml batchRequest = container.getBatchRequest();
283
284        if ( batchRequest == null )
285        {
286            throw new XmlPullParserException( I18n.err( I18n.ERR_03003_UNABLE_TO_FIND_BATCH_REQUEST ), container.getParser(), null );
287        }
288
289        return ( batchRequest.getProcessing() == Processing.PARALLEL ) && ( batchRequest.getResponseOrder() == ResponseOrder.UNORDERED );
290    }
291
292
293    /**
294     * XML Pretty Printer XSLT Transformation
295     *
296     * @param document the Dom4j Document
297     * @return the transformed document
298     */
299    public static Document styleDocument( Document document )
300    {
301        // load the transformer using JAXP
302        TransformerFactory factory = TransformerFactory.newInstance();
303        Transformer transformer = null;
304
305        try
306        {
307            factory.setFeature( javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE );
308            try
309            {
310                factory.setAttribute( javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD, Strings.EMPTY_STRING );
311                factory.setAttribute( javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET, Strings.EMPTY_STRING );
312            }
313            catch ( IllegalArgumentException ex )
314            {
315                // ignore
316            }
317            transformer = factory.newTransformer( new StreamSource( ParserUtils.class
318                .getResourceAsStream( "/org/apache/directory/shared/dsmlv2/DSMLv2.xslt" ) ) );
319        }
320        catch ( TransformerConfigurationException e1 )
321        {
322            if ( LOG.isWarnEnabled() )
323            {
324                LOG.warn( I18n.msg( I18n.MSG_3000_FAILED_TO_CREATE_XSLT_TRANSFORMER ), e1 );
325            }
326
327            // return original document
328            return document;
329        }
330
331        // now lets style the given document
332        DocumentSource source = new DocumentSource( document );
333        DocumentResult result = new DocumentResult();
334
335        try
336        {
337            transformer.transform( source, result );
338        }
339        catch ( TransformerException e )
340        {
341            // return original document
342            return document;
343        }
344
345        // return the transformed document
346        return result.getDocument();
347    }
348}