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 javax.lang.model.element.ElementKind.METHOD;
021import static javax.tools.Diagnostic.Kind.ERROR;
022import static org.apiguardian.api.API.Status.INTERNAL;
023import static org.tquadrat.foundation.i18n.I18nUtil.composeMessageKey;
024import static org.tquadrat.foundation.i18n.I18nUtil.composeTextKey;
025import static org.tquadrat.foundation.i18n.TextUse.NAME;
026import static org.tquadrat.foundation.i18n.TextUse.STRING;
027import static org.tquadrat.foundation.i18n.TextUse.TEXTUSE_DEFAULT;
028import static org.tquadrat.foundation.i18n.TextUse.TXT;
029import static org.tquadrat.foundation.lang.Objects.nonNull;
030import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
031import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
032import static org.tquadrat.foundation.util.JavaUtils.isAddMethod;
033import static org.tquadrat.foundation.util.JavaUtils.isGetter;
034import static org.tquadrat.foundation.util.JavaUtils.isSetter;
035import static org.tquadrat.foundation.util.JavaUtils.retrievePropertyName;
036import static org.tquadrat.foundation.util.StringUtils.capitalize;
037import static org.tquadrat.foundation.util.SystemUtils.retrieveLocale;
038
039import javax.lang.model.element.Element;
040import javax.lang.model.element.ExecutableElement;
041import javax.lang.model.element.TypeElement;
042import javax.lang.model.element.VariableElement;
043import javax.lang.model.util.Elements;
044import javax.lang.model.util.SimpleElementVisitor9;
045import java.util.Locale;
046import java.util.Map;
047import java.util.SortedMap;
048import java.util.TreeMap;
049
050import org.apiguardian.api.API;
051import org.tquadrat.foundation.annotation.ClassVersion;
052import org.tquadrat.foundation.ap.APHelper;
053import org.tquadrat.foundation.ap.IllegalAnnotationError;
054import org.tquadrat.foundation.exception.UnsupportedEnumError;
055import org.tquadrat.foundation.i18n.Message;
056import org.tquadrat.foundation.i18n.Text;
057import org.tquadrat.foundation.i18n.TextUse;
058import org.tquadrat.foundation.i18n.Texts;
059import org.tquadrat.foundation.i18n.Translation;
060
061/**
062 *  A visitor class that collects the texts for resource bundle properties
063 *  files from the annotations.
064 *
065 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
066 *  @version $Id: TextCollector.java 1062 2023-09-25 23:11:41Z tquadrat $
067 *  @since 0.0.2
068 *
069 *  @UMLGraph.link
070 */
071@ClassVersion( sourceVersion = "$Id: TextCollector.java 1062 2023-09-25 23:11:41Z tquadrat $" )
072@API( status = INTERNAL, since = "0.1.0" )
073public class TextCollector extends SimpleElementVisitor9<Void,Map<Locale,SortedMap<String,TextEntry>>>
074{
075        /*------------*\
076    ====** Attributes **=======================================================
077        \*------------*/
078    /**
079     *  Some helper utilities for the work with
080     *  {@link Element}
081     *  instances.
082     */
083    private final Elements m_ElementUtils;
084
085    /**
086     *  The prefix for the message ids.
087     */
088    private final String m_MessagePrefix;
089
090    /**
091     *  The processing environment.
092     */
093    private final APHelper m_Environment;
094
095        /*--------------*\
096    ====** Constructors **=====================================================
097        \*--------------*/
098    /**
099     *  Creates a new {@code TextCollector} instance.
100     *
101     *  @param  environment The processing environment for the annotation
102     *      processor.
103     *  @param  messagePrefix   The configured message prefix.
104     */
105    public TextCollector( final APHelper environment, final String messagePrefix )
106    {
107        m_Environment = requireNonNullArgument( environment, "environment" );
108        m_ElementUtils = m_Environment.getElementUtils();
109
110        m_MessagePrefix = requireNotEmptyArgument( messagePrefix, "messagePrefix" );
111    }   // TextCollector()
112
113        /*---------*\
114    ====** Methods **==========================================================
115        \*---------*/
116    /**
117     *  Adds the text entries to the texts map.
118     *
119     *  @param  texts   The texts map.
120     *  @param  element The annotated element.
121     *  @param  key The resource bundle key.
122     *  @param  description The description for the text.
123     *  @param  className   The fully qualified name of the class that defines
124     *      the text.
125     *  @param  isMessage   {@code true} if the text is a message,
126     *      {@code false} otherwise.
127     *  @param  translations    The texts in the various languages.
128     */
129    @SuppressWarnings( {"MethodCanBeVariableArityMethod", "OptionalGetWithoutIsPresent", "MethodWithTooManyParameters"} )
130    private final void addTextEntry( @SuppressWarnings( "BoundedWildcard" ) final Map<Locale,SortedMap<String,TextEntry>> texts, final Element element, final String key, final String description, final String className, final boolean isMessage, final Translation [] translations )
131    {
132        for( final var translation : requireNonNullArgument( translations, "translations" ) )
133        {
134            //---* Create the entry *------------------------------------------
135            final var locale = retrieveLocale( translation.language() ).get();
136            final var text = translation.text();
137            final var entry = new TextEntry( key, isMessage, locale, description, text, className );
138
139            //---* Get the text map *------------------------------------------
140            final var textMap = texts.computeIfAbsent( locale, currentLocale -> new TreeMap<>() );
141
142            //---* Add the entry *---------------------------------------------
143            if( textMap.containsKey( key ) )
144            {
145                final var message =
146                    """
147                    Key '%s' is not unique (Annotation: %s in class '%s')
148                    Check translation locale in case the key is not a real duplicate"""
149                        .formatted( key, isMessage ? "@Message" : "@Text", className );
150                m_Environment.printMessage( ERROR, message, element );
151                throw new IllegalAnnotationError( message );
152            }
153            textMap.put( key, entry );
154        }
155    }   //  addTextEntry()
156
157    /**
158     *  Processes a text annotation.
159     *
160     *  @param  texts   The texts map.
161     *  @param  element The annotated element.
162     *  @param  annotation  The text annotation.
163     *  @param  className   The fully qualified name of the class that defines
164     *      the text.
165     */
166    @SuppressWarnings( {"OverlyNestedMethod", "OverlyComplexMethod"} )
167    private final void processTextAnnotation( final Map<Locale,SortedMap<String,TextEntry>> texts, final Element element, final Text annotation, final String className )
168    {
169        //---* The description of the text *-----------------------------------
170        final var description = annotation.description();
171
172        //---* The text use and id *-------------------------------------------
173        var textUse = annotation.use();
174        var id = annotation.id();
175        KindSwitch: switch( element.getKind() )
176        {
177            case ENUM_CONSTANT ->
178            {
179                if( textUse == TEXTUSE_DEFAULT ) textUse = STRING;
180                if( id.isBlank() ) id = element.getSimpleName().toString();
181            }
182
183            case METHOD ->
184            {
185                if( id.isBlank() )
186                {
187                    if( isGetter( element ) || isSetter( element ) || isAddMethod( element ) )
188                    {
189                        /*
190                         * Only for getters, setters and "add" methods, we can
191                         * infer the id from the name of the property.
192                         */
193                        id = capitalize( retrievePropertyName( (ExecutableElement) element ) );
194                        if( textUse == TEXTUSE_DEFAULT ) textUse = NAME;
195                    }
196                    else
197                    {
198                        final var message = "Missing id for Element '%s'".formatted( element.getSimpleName().toString() );
199                        m_Environment.printMessage( ERROR, message, element );
200                        throw new IllegalAnnotationError( message );
201                    }
202                }
203            }
204
205            case FIELD ->
206            {
207                if( id.isBlank() )
208                {
209                    /*
210                     * Get the name of the variable that is annotated; this is
211                     * used as the id for the text if none is set explicitly.
212                     */
213                    id = element.getSimpleName().toString();
214                    if( id.startsWith( "m_" ) )
215                    {
216                        id = id.substring( 2 );
217                    }
218                    else
219                    {
220                        final var pos = id.indexOf( '_' );
221                        if( pos > 1 )
222                        {
223                            try
224                            {
225                                textUse = TextUse.valueOf( id.substring( 0, pos ) );
226                            }
227                            catch( final IllegalArgumentException e )
228                            {
229                                final var message = "Id '%s' is invalid: %s".formatted( id, e.toString() );
230                                m_Environment.printMessage( ERROR, message, element );
231                                throw new IllegalAnnotationError( message, e );
232                            }
233                            id = id.substring( pos + 1 );
234                        }
235                    }
236                }
237            }
238
239            //$CASES-OMITTED$
240            default -> throw new UnsupportedEnumError( element.getKind() );
241        }   //  KindSwitch:
242
243        if( textUse == TEXTUSE_DEFAULT ) textUse = TXT;
244
245        //---* Add the new text *----------------------------------------------
246        addTextEntry( texts, element, composeTextKey( className, textUse, id ), description, className, false, annotation.translations() );
247    }   //  processTextAnnotation()
248
249    /**
250     *  {@inheritDoc}
251     */
252    @Override
253    public final Void visitExecutable( final ExecutableElement element, final Map<Locale,SortedMap<String,TextEntry>> texts )
254    {
255        if( element.getKind() == METHOD )
256        {
257            //---* Get the defining class *------------------------------------
258            final var definingClass = (TypeElement) element.getEnclosingElement();
259            final var className = m_ElementUtils.getBinaryName( definingClass ).toString();
260
261            // ---* Get the annotation *------------------------------------
262            final var textAnnotation = element.getAnnotation( Text.class );
263            final var textsAnnotation = element.getAnnotation( Texts.class );
264
265            if( nonNull( textsAnnotation ) )
266            {
267                for( final var annotation : textsAnnotation.value() )
268                {
269                    processTextAnnotation( texts, element, annotation, className );
270                }
271            }
272            else if( nonNull( textAnnotation ) )
273            {
274                processTextAnnotation( texts, element, textAnnotation, className );
275            }
276        }
277
278        //---* Done *----------------------------------------------------------
279        return defaultAction( element, texts );
280    }   //  visitExecutable()
281
282    /**
283     * {@inheritDoc}
284     */
285    @Override
286    public final Void visitVariable( final VariableElement element, final Map<Locale,SortedMap<String,TextEntry>> texts )
287    {
288        if( element.getKind().isField() )
289        {
290            //---* Get the defining class *------------------------------------
291            final var definingClass = (TypeElement) element.getEnclosingElement();
292            final var className = m_ElementUtils.getBinaryName( definingClass ).toString();
293
294            // ---* Get the annotation *------------------------------------
295            final var msgAnnotation = element.getAnnotation( Message.class );
296            final var textAnnotation = element.getAnnotation( Text.class );
297            final var textsAnnotation = element.getAnnotation( Texts.class );
298
299            if( nonNull( msgAnnotation ) )
300            {
301                /*
302                 * Get the constant value for the variable; this is used as the
303                 * id for the message.
304                 */
305                final var value = element.getConstantValue();
306
307                /*
308                 * Get the name of the variable that is annotated; this is used
309                 * as the id for the message if none is set explicitly.
310                 */
311                final var variableName = element.getSimpleName();
312
313                //---* The description of the message *------------------------
314                final var description = msgAnnotation.description();
315
316                //---* The message id *----------------------------------------
317                String key = null;
318                if( value instanceof final Integer msgId )
319                {
320                    key = composeMessageKey( m_MessagePrefix, msgId.intValue() );
321                }
322                else if( nonNull( value ) )
323                {
324                    key = composeMessageKey( m_MessagePrefix, value.toString() );
325                }
326                else
327                {
328                    key = composeMessageKey( m_MessagePrefix, variableName.toString() );
329                }
330
331                //---* Add the new text *--------------------------------------
332                addTextEntry( texts, element, key, description, className, true, msgAnnotation.translations() );
333            }
334
335            if( nonNull( textAnnotation ) )
336            {
337                processTextAnnotation( texts, element, textAnnotation, className );
338            }
339
340            if( nonNull( textsAnnotation ) )
341            {
342                for( final var annotation : textsAnnotation.value() )
343                {
344                    processTextAnnotation( texts, element, annotation, className );
345                }
346            }
347        }
348
349        //---* Done *----------------------------------------------------------
350        return defaultAction( element, texts );
351    }   // visitVariable()
352}
353// class TextCollector
354
355/*
356 *  End of File
357 */