001/* 002 * ============================================================================ 003 * Copyright © 2002-2023 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * Licensed to the public under the agreements of the GNU Lesser General Public 007 * License, version 3.0 (the "License"). You may obtain a copy of the License at 008 * 009 * http://www.gnu.org/licenses/lgpl.html 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 014 * License for the specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package org.tquadrat.foundation.i18n.ap; 019 020import static java.util.stream.Collectors.joining; 021import static org.apiguardian.api.API.Status.INTERNAL; 022import static org.tquadrat.foundation.i18n.I18nUtil.ADDITIONAL_TEXT_FILE; 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.lang.Objects.requireNonNullArgument; 027import static org.tquadrat.foundation.util.StringUtils.stream; 028import static org.tquadrat.foundation.util.SystemUtils.retrieveLocale; 029 030import java.io.IOException; 031import java.io.StringReader; 032import java.util.Locale; 033import java.util.Map; 034import java.util.SortedMap; 035import java.util.TreeMap; 036 037import org.apiguardian.api.API; 038import org.tquadrat.foundation.annotation.ClassVersion; 039import org.xml.sax.Attributes; 040import org.xml.sax.InputSource; 041import org.xml.sax.Locator; 042import org.xml.sax.SAXException; 043import org.xml.sax.SAXParseException; 044import org.xml.sax.helpers.DefaultHandler; 045 046/** 047 * The implementation for a 048 * {@link DefaultHandler} 049 * that handles the files for additional text resources. 050 * 051 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 052 * @version $Id: TextFileContentHandler.java 1062 2023-09-25 23:11:41Z tquadrat $ 053 * @since 0.1.0 054 * 055 * @UMLGraph.link 056 */ 057@ClassVersion( sourceVersion = "$Id: TextFileContentHandler.java 1062 2023-09-25 23:11:41Z tquadrat $" ) 058@API( status = INTERNAL, since = "0.1.0" ) 059public final class TextFileContentHandler extends DefaultHandler 060{ 061 /*------------*\ 062 ====** Attributes **======================================================= 063 \*------------*/ 064 /** 065 * The key for the current text entry. 066 */ 067 private String m_CurrentKey = null; 068 069 /** 070 * The description for the current text entry. 071 */ 072 @SuppressWarnings( "StringBufferField" ) 073 private final StringBuilder m_CurrentDescription = new StringBuilder(); 074 075 /** 076 * The locale for the current translation. 077 */ 078 private Locale m_CurrentLocale = null; 079 080 /** 081 * The current text. 082 */ 083 @SuppressWarnings( "StringBufferField" ) 084 private final StringBuilder m_CurrentText = new StringBuilder(); 085 086 /** 087 * Flag that indicates if currently a description is being processed. 088 */ 089 private boolean m_IsDescription = false; 090 091 /** 092 * The document locator. 093 */ 094 @SuppressWarnings( {"unused", "FieldCanBeLocal"} ) 095 private Locator m_Locator = null; 096 097 /** 098 * The map that holds the texts. The key of this map is the locale for the 099 * text or message translation, while the values are Maps with the text 100 * entries itself, using the message or text id as the id. 101 */ 102 private final Map<Locale,SortedMap<String,TextEntry>> m_Texts; 103 104 /*--------------*\ 105 ====** Constructors **===================================================== 106 \*--------------*/ 107 /** 108 * Creates a new {@code TextFileContentHandler} instance. 109 * 110 * @param texts The texts for the resources. 111 */ 112 public TextFileContentHandler( final Map<Locale,SortedMap<String,TextEntry>> texts ) 113 { 114 m_Texts = requireNonNullArgument( texts, "texts" ); 115 } // TextFileContentHandler() 116 117 /*---------*\ 118 ====** Methods **========================================================== 119 \*---------*/ 120 /** 121 * {@inheritDoc} 122 */ 123 @Override 124 public final void characters( final char [] ch, final int start, final int length ) throws SAXException 125 { 126 if( length > 0 ) 127 { 128 final var text = new String( ch, start, length ).lines().collect( joining() ); 129 if( m_IsDescription ) 130 { 131 if( !text.isBlank() ) m_CurrentDescription.append( text.trim() ); 132 } 133 else 134 { 135 m_CurrentText.append( text ); 136 } 137 } 138 } // characters() 139 140 /** 141 * {@inheritDoc} 142 */ 143 @Override 144 public final void endElement( final String uri, final String localName, final String qName ) throws SAXException 145 { 146 switch( localName ) 147 { 148 //---* The root element *------------------------------------------ 149 case "texts" -> 150 { 151 m_CurrentKey = null; 152 m_CurrentDescription.setLength( 0 ); 153 m_CurrentLocale = null; 154 m_CurrentText.setLength( 0 ); 155 m_IsDescription = false; 156 } 157 158 //---* A single text entry *--------------------------------------- 159 case "text" -> 160 { 161 m_CurrentKey = null; 162 m_CurrentDescription.setLength( 0 ); 163 m_CurrentText.setLength( 0 ); 164 m_IsDescription = false; 165 } 166 167 //---* A text resource description *------------------------------- 168 case "description" -> m_IsDescription = false; 169 170 //---* A text resource description *------------------------------- 171 case "translation" -> 172 { 173 final var translations = m_Texts.computeIfAbsent( m_CurrentLocale, locale -> new TreeMap<>() ); 174 translations.put( m_CurrentKey, new TextEntry( m_CurrentKey, false, m_CurrentLocale, m_CurrentDescription.toString(), m_CurrentText.toString(), ADDITIONAL_TEXT_FILE ) ); 175 176 m_CurrentLocale = null; 177 m_CurrentText.setLength( 0 ); 178 m_IsDescription = false; 179 } 180 181 default -> throw new SAXParseException( "Unknown element: %s".formatted( localName ), m_Locator ); 182 } 183 } // - endElement() 184 185 /** 186 * {@inheritDoc} 187 */ 188 @Override 189 public final void ignorableWhitespace( final char [] ch, final int start, final int length ) throws SAXException 190 { 191 if( (length > 0) && !m_IsDescription ) 192 { 193 stream( new String( ch, start, length ), '\n' ).forEach( m_CurrentText::append ); 194 } 195 } // ignorableWhitespace() 196 197 /** 198 * {@inheritDoc} 199 */ 200 @Override 201 public final InputSource resolveEntity( final String publicId, final String systemId ) throws IOException, SAXException 202 { 203 InputSource retValue = null; 204 205 if( nonNull( systemId ) && "http://dtd.tquadrat.org/AdditionalText.dtd".equals( systemId ) ) 206 { 207 try( final var inputStream = getClass().getResourceAsStream( "/AdditionalText.dtd" ) ) 208 { 209 if( nonNull( inputStream ) ) 210 { 211 final var buffer = new String( inputStream.readAllBytes(), UTF8 ); 212 retValue = new InputSource( new StringReader( buffer ) ); 213 } 214 } 215 } 216 217 if( isNull( retValue ) ) retValue = super.resolveEntity( publicId, systemId ); 218 219 //---* Done *---------------------------------------------------------- 220 return retValue; 221 } // resolveEntity() 222 223 /** 224 * {@inheritDoc} 225 */ 226 @Override 227 public final void setDocumentLocator( final Locator locator ) { m_Locator = locator; } 228 229 /** 230 * {@inheritDoc} 231 */ 232 @Override 233 public final void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) throws SAXException 234 { 235 switch( localName ) 236 { 237 //---* The root element *------------------------------------------ 238 case "texts" -> 239 { 240 m_CurrentKey = null; 241 m_CurrentDescription.setLength( 0 ); 242 m_CurrentLocale = null; 243 m_CurrentText.setLength( 0 ); 244 m_IsDescription = false; 245 } 246 247 //---* A single text entry *--------------------------------------- 248 case "text" -> 249 { 250 m_CurrentKey = attributes.getValue( "key" ); 251 m_CurrentDescription.setLength( 0 ); 252 m_CurrentLocale = null; 253 m_CurrentText.setLength( 0 ); 254 m_IsDescription = false; 255 } 256 257 //---* A text resource description *------------------------------- 258 case "description" -> 259 { 260 m_CurrentDescription.setLength( 0 ); 261 m_IsDescription = true; 262 } 263 264 //---* A text translation *---------------------------------------- 265 case "translation" -> 266 { 267 m_CurrentText.setLength( 0 ); 268 final var language = attributes.getValue( "language" ); 269 //noinspection OptionalGetWithoutIsPresent 270 m_CurrentLocale = retrieveLocale( language ).get(); 271 m_IsDescription = false; 272 } 273 274 default -> throw new SAXParseException( "Unknown element: %s".formatted( localName ), m_Locator ); 275 } 276 } // startElement() 277} 278// class TextFileContentHandler 279 280/* 281 * End of File 282 */