001/*
002 * ============================================================================
003 * Copyright © 2002-2026 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 *
007 * Licensed to the public under the agreements of the GNU Lesser General Public
008 * License, version 3.0 (the "License"). You may obtain a copy of the License at
009 *
010 *      http://www.gnu.org/licenses/lgpl.html
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015 * License for the specific language governing permissions and limitations
016 * under the License.
017 */
018
019package org.tquadrat.foundation.xml.stringconverter;
020
021import static java.lang.String.format;
022import static org.apiguardian.api.API.Status.STABLE;
023import static org.tquadrat.foundation.lang.CommonConstants.UTF8;
024import static org.tquadrat.foundation.lang.Objects.isNull;
025import static org.tquadrat.foundation.lang.Objects.nonNull;
026import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
027
028import javax.xml.parsers.DocumentBuilderFactory;
029import javax.xml.parsers.ParserConfigurationException;
030import javax.xml.transform.TransformerConfigurationException;
031import javax.xml.transform.TransformerException;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.dom.DOMSource;
034import javax.xml.transform.stream.StreamResult;
035import java.io.ByteArrayOutputStream;
036import java.io.IOException;
037import java.io.Serial;
038import java.io.StringReader;
039import java.io.UnsupportedEncodingException;
040import java.util.Collection;
041import java.util.List;
042
043import org.apiguardian.api.API;
044import org.tquadrat.foundation.annotation.ClassVersion;
045import org.tquadrat.foundation.exception.UnlikelyExceptionError;
046import org.tquadrat.foundation.lang.StringConverter;
047import org.tquadrat.foundation.xml.parse.NullErrorHandler;
048import org.w3c.dom.Document;
049import org.xml.sax.InputSource;
050import org.xml.sax.SAXException;
051
052/**
053 *  <p>{@summary An implementation of
054 *  {@link StringConverter}
055 *  for
056 *  {@link Document}
057 *  values.}</p>
058 *  <p>The method
059 *  {@link #fromString(CharSequence)}
060 *  parses the input to a document, using the default
061 *  {@link DocumentBuilderFactory}
062 *  to get a
063 *  {@link javax.xml.parsers.DocumentBuilder}.</p>
064 *  <p>{@link #toString(Document)}
065 *  uses an instance of
066 *  {@link javax.xml.transform.Transformer}
067 *  to get a String from the document.</p>
068 *
069 *  @note When converting a String to an instance of {@code Document} back to a
070 *      String, the final String may be different from the input because the
071 *      formatting can differ, and some implicit defaults will now be provided
072 *      as explicit setting. Only the semantics are guaranteed to be still the
073 *      same.
074 *
075 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
076 *  @version $Id: DocumentStringConverter.java 1072 2023-09-30 20:44:38Z tquadrat $
077 *  @since 0.1.0
078 *
079 *  @UMLGraph.link
080 */
081@ClassVersion( sourceVersion = "$Id: DocumentStringConverter.java 1072 2023-09-30 20:44:38Z tquadrat $" )
082@API( status = STABLE, since = "0.1.0" )
083@SuppressWarnings( "exports" )
084public final class DocumentStringConverter implements StringConverter<Document>
085{
086        /*-----------*\
087    ====** Constants **========================================================
088        \*-----------*/
089    /**
090     *  The error message for an invalid XML contents: {@value}.
091     */
092    public static final String MSG_InvalidXML = "Invalid XML: %1$s";
093
094        /*------------------------*\
095    ====** Static Initialisations **===========================================
096        \*------------------------*/
097    /**
098     *  The serial version UID for objects of this class: {@value}.
099     *
100     *  @hidden
101     */
102    @Serial
103    private static final long serialVersionUID = 1L;
104
105    /**
106     *  An instance of this class.
107     */
108    public static final DocumentStringConverter INSTANCE = new DocumentStringConverter();
109
110        /*--------------*\
111    ====** Constructors **=====================================================
112        \*--------------*/
113    /**
114     *  <p>{@summary {@constructor}}</p>
115     */
116    public DocumentStringConverter() { super(); }
117
118        /*---------*\
119    ====** Methods **==========================================================
120        \*---------*/
121    /**
122     *  {@inheritDoc}
123     */
124    @Override
125    public final Document fromString( final CharSequence source ) throws IllegalArgumentException
126    {
127        Document retValue = null;
128        if( nonNull( source ) )
129        {
130            try
131            {
132                @SuppressWarnings( "LocalVariableNamingConvention" )
133                final var documentBuilderFactory = DocumentBuilderFactory.newInstance();
134                documentBuilderFactory.setNamespaceAware( true );
135                documentBuilderFactory.setValidating( false );
136                documentBuilderFactory.setIgnoringComments( false );
137                documentBuilderFactory.setIgnoringElementContentWhitespace( true );
138                documentBuilderFactory.setCoalescing( false );
139                documentBuilderFactory.setExpandEntityReferences( false );
140                final var builder = documentBuilderFactory.newDocumentBuilder();
141                builder.setErrorHandler( NullErrorHandler.INSTANCE );
142                retValue = builder.parse( new InputSource( new StringReader( source.toString() ) ) );
143                retValue.normalizeDocument();
144            }
145            catch( final ParserConfigurationException e )
146            {
147                throw new UnlikelyExceptionError( "Cannot instantiate DocumentBuilder", e );
148            }
149            catch( final SAXException e )
150            {
151                throw new IllegalArgumentException( format( MSG_InvalidXML, source ), e );
152            }
153            catch( final IOException e )
154            {
155                throw new UnlikelyExceptionError( e );
156            }
157        }
158
159        //---* Done *----------------------------------------------------------
160        return retValue;
161    }   //  fromString()
162
163    /**
164     *  Provides the subject class for this converter.
165     *
166     * @return The subject class.
167     */
168    @SuppressWarnings( "PublicMethodNotExposedInInterface" )
169    public final Collection<Class<? extends Document>> getSubjectClass() { return List.of( Document.class ); }
170
171    /**
172     *  This method is used by the
173     *  {@link java.util.ServiceLoader}
174     *  to obtain the instance for this
175     *  {@link org.tquadrat.foundation.lang.StringConverter}
176     *  implementation.
177     *
178     *  @return The instance for this {@code StringConverter} implementation.
179     */
180    public static final DocumentStringConverter provider() { return INSTANCE; }
181
182    /**
183     *  {@inheritDoc}
184     */
185    @SuppressWarnings( "OverlyComplexMethod" )
186    @Override
187    public final String toString( final Document source )
188    {
189        String retValue = null;
190        if( nonNull( source ) )
191        {
192            final var encoding = source.getXmlEncoding();
193            try
194            {
195                //---* Obtain the transformer *--------------------------------
196                final var transformerFactory = TransformerFactory.newInstance();
197                final var transformer = transformerFactory.newTransformer();
198
199                //---* Configure the transformer *-----------------------------
200                transformer.setOutputProperty( "method", "xml" );
201                transformer.setOutputProperty( "indent", "no" );
202                transformer.setOutputProperty( "standalone", source.getXmlStandalone() ? "yes" : "no" );
203                if( nonNull( encoding ) ) transformer.setOutputProperty( "encoding", encoding );
204                final var doctype = source.getDoctype();
205                if( nonNull( doctype) )
206                {
207                    final var systemId = doctype.getSystemId();
208                    if( isNotEmptyOrBlank( systemId ) ) transformer.setOutputProperty( "doctype-system", systemId );
209                    final var publicId = doctype.getPublicId();
210                    if( isNotEmptyOrBlank( publicId ) ) transformer.setOutputProperty( "doctype-public", publicId );
211                }
212
213                final var documentSource = new DOMSource( source );
214                final var outputStream = new ByteArrayOutputStream();
215                final var result = new StreamResult( outputStream );
216                transformer.transform( documentSource, result );
217                retValue = outputStream.toString( isNull( encoding ) ? UTF8.name() : encoding );
218            }
219            catch( final TransformerConfigurationException e )
220            {
221                throw new UnlikelyExceptionError( "Cannot instantiate Transformer", e );
222            }
223            catch( final TransformerException e )
224            {
225                throw new IllegalArgumentException( "Unrecoverable error on transformation", e );
226            }
227            catch( final UnsupportedEncodingException e )
228            {
229                throw new UnlikelyExceptionError( "Invalid encoding: %s".formatted( encoding ), e );
230            }
231        }
232
233        //---* Done *----------------------------------------------------------
234        return retValue;
235    }   //  toString()
236}
237//  class DocumentStringConverter
238
239/*
240 *  End of File
241 */