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