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 */