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