001/* 002 * ============================================================================ 003 * Copyright © 2015 Square, Inc. 004 * Copyright for the modifications © 2018-2025 by Thomas Thrien. 005 * ============================================================================ 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019 020package org.tquadrat.foundation.javacomposer; 021 022import static java.lang.Character.charCount; 023import static java.lang.Character.isJavaIdentifierPart; 024import static java.lang.Character.isJavaIdentifierStart; 025import static org.apiguardian.api.API.Status.STABLE; 026import static org.tquadrat.foundation.lang.Objects.isNull; 027import static org.tquadrat.foundation.lang.Objects.nonNull; 028import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 029import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument; 030import static org.tquadrat.foundation.util.UniqueIdUtils.randomUUID; 031 032import javax.lang.model.SourceVersion; 033import java.util.HashMap; 034import java.util.HashSet; 035import java.util.Map; 036import java.util.Set; 037 038import org.apiguardian.api.API; 039import org.tquadrat.foundation.annotation.ClassVersion; 040import org.tquadrat.foundation.annotation.NotRecord; 041import org.tquadrat.foundation.exception.ValidationException; 042 043/** 044 * <p>{@summary Assigns Java identifier names to avoid collisions, 'abuse' of 045 * keywords, and invalid characters.} To use it, first create an instance of 046 * this class and allocate all the names that are needed. Typically, this is 047 * a mix of user-supplied names and constants:</p> 048 * <pre><code> NameAllocator nameAllocator = new NameAllocator(); 049 * for( final var property : properties ) 050 * { 051 * nameAllocator.newName( property.name(), property ); 052 * } 053 * nameAllocator.newName( "sb", "string builder" );</code></pre> 054 * <p>Pass a unique tag object to each allocation. The tag scopes the name, 055 * and can be used to look up the allocated name later. Typically, the tag is 056 * the object that is being named. In the above example we use 057 * {@code property} for the user-supplied property names, and 058 * {@code "string builder"} for our constant string builder.</p> 059 * <p>Once we've allocated names we can use them when generating code:</p> 060 * <pre><code> MethodSpec.Builder builder = MethodSpec.methodBuilder( "toString" ) 061 * .addAnnotation( Override.class ) 062 * .addModifiers( Modifier.PUBLIC ) 063 * .returns( String.class ); 064 * 065 * builder.addStatement( "$1T $2N = new $1T()", StringBuilder.class, nameAllocator.get( "string builder" ) ); 066 * for( var property : properties ) 067 * { 068 * builder.addStatement( "$N.append( $N )", nameAllocator.get( "string builder" ), nameAllocator.get( property ) ); 069 * } 070 * builder.addStatement( "return $N", nameAllocator.get( "string builder" ) ); 071 * return builder.build();</code></pre> 072 * <p>The above code generates unique names if presented with conflicts. Given 073 * user-supplied properties with names {@code ab} and {@code sb} this 074 * generates the following code:</p> 075 * <pre><code> @Override 076 * public String toString() 077 * { 078 * StringBuilder sb_ = new StringBuilder(); 079 * sb_.append( ab ); 080 * sb_.append( sb ); 081 * return sb_.toString(); 082 * }</code></pre> 083 * <p>The underscore is appended to {@code sb} to avoid conflicting with the 084 * user-supplied {@code sb} property. Underscores are also prefixed for names 085 * that start with a digit, and used to replace name-unsafe characters like 086 * space or dash.</p> 087 * <p>When dealing with multiple independent inner scopes, use a 088 * {@link #clone()} 089 * of the {@code NameAllocator} used for the outer scope to further refine 090 * name allocation for a specific inner scope.</p> 091 * 092 * @author Square,Inc. 093 * @modified Thomas Thrien - thomas.thrien@tquadrat.org 094 * @version $Id: NameAllocator.java 1151 2025-10-01 21:32:15Z tquadrat $ 095 * @since 0.0.5 096 * 097 * @UMLGraph.link 098 */ 099@ClassVersion( sourceVersion = "$Id: NameAllocator.java 1151 2025-10-01 21:32:15Z tquadrat $" ) 100@API( status = STABLE, since = "0.0.5" ) 101@NotRecord 102public final class NameAllocator implements Cloneable 103{ 104 /*------------*\ 105 ====** Attributes **======================================================= 106 \*------------*/ 107 /** 108 * The allocated names. 109 */ 110 private final Set<String> m_AllocatedNames; 111 112 /** 113 * The registry for the names. 114 */ 115 private final Map<Object,String> m_TagToName; 116 117 /*--------------*\ 118 ====** Constructors **===================================================== 119 \*--------------*/ 120 /** 121 * Creates a new {@code NameAllocator} instance. 122 */ 123 public NameAllocator() { this( new HashSet<>(), new HashMap<>() ); } 124 125 /** 126 * Creates a new {@code NameAllocator} instance. 127 * 128 * @param allocatedNames The allocated names. 129 * @param tagToName The registry for names. 130 */ 131 @SuppressWarnings( {"CollectionDeclaredAsConcreteClass", "TypeMayBeWeakened"} ) 132 private NameAllocator( final HashSet<String> allocatedNames, final HashMap<Object,String> tagToName ) 133 { 134 m_AllocatedNames = allocatedNames; 135 m_TagToName = tagToName; 136 } // NameAllocator() 137 138 /*---------*\ 139 ====** Methods **========================================================== 140 \*---------*/ 141 /** 142 * Translates the given suggestion for an identifier to a valid Java 143 * identifier by replacing invalid characters by an underscore 144 * ("_"). If the {@code suggestion} starts with a character that 145 * is not allowed to start a Java identifier, but is otherwise valid, the 146 * resulting identifier is prepended by an underscore. 147 * 148 * @param suggestion The suggestion for an identifier. 149 * @return A valid Java identifier. 150 */ 151 public static final String toJavaIdentifier( final String suggestion ) 152 { 153 final var buffer = new StringBuilder(); 154 var codePoint = requireNotEmptyArgument( suggestion, "suggestion" ).codePointAt( 0 ); 155 if( isJavaIdentifierStart( codePoint ) ) 156 { 157 buffer.appendCodePoint( codePoint ); 158 } 159 else 160 { 161 buffer.append( '_' ); 162 if( isJavaIdentifierPart( codePoint ) ) buffer.appendCodePoint( codePoint ); 163 } 164 //noinspection ForLoopWithMissingComponent 165 for( var i = charCount( codePoint ); i < suggestion.length(); ) 166 { 167 codePoint = suggestion.codePointAt( i ); 168 buffer.appendCodePoint( isJavaIdentifierPart( codePoint ) ? codePoint : '_' ); 169 i += charCount( codePoint ); 170 } 171 final var retValue = buffer.toString(); 172 173 //---* Done *---------------------------------------------------------- 174 return retValue; 175 } // toJavaIdentifier() 176 177 /** 178 * Creates a deep copy of this {@code NameAllocator}. Useful to create 179 * multiple independent refinements of a {@code NameAllocator} to be used 180 * in the respective definition of multiples, independently-scoped, inner 181 * code blocks. 182 * 183 * @return A deep copy of this NameAllocator. 184 */ 185 @SuppressWarnings( "MethodDoesntCallSuperMethod" ) 186 @Override 187 public final NameAllocator clone() 188 { 189 final var retValue = new NameAllocator( new HashSet<>( m_AllocatedNames ), new HashMap<>( m_TagToName ) ); 190 191 //---* Done *---------------------------------------------------------- 192 return retValue; 193 } // clone() 194 195 /** 196 * Retrieves a name that was previously created with 197 * {@link #newName(String, Object)}. 198 * 199 * @param tag The identifier for the name. 200 * @return The name. 201 * @throws ValidationException The tag was unknown. 202 */ 203 public final String get( final Object tag ) 204 { 205 final var retValue = m_TagToName.get( requireNonNullArgument( tag, "tag" ) ); 206 if( isNull( retValue ) ) throw new ValidationException( "unknown tag: " + tag ); 207 208 //---* Done *---------------------------------------------------------- 209 return retValue; 210 } // get() 211 212 /** 213 * Returns a new name using the given suggestion that will not be a Java 214 * keyword or clash with other names. 215 * 216 * @param suggestion The suggestion. 217 * @return The new name. 218 */ 219 public final String newName( final String suggestion ) 220 { 221 return newName( suggestion, randomUUID().toString() ); 222 } // newName() 223 224 /** 225 * Returns a new name based on the given suggestion that will not be a 226 * Java keyword or clash with other names. The returned value can be 227 * queried multiple times by passing the given tag to 228 * {@link #get(Object)}. 229 * 230 * @param suggestion The suggestion for the new name. 231 * @param tag The tag for the new name. 232 * @return The new name. 233 */ 234 public final String newName( final String suggestion, final Object tag ) 235 { 236 requireNonNullArgument( tag, "tag" ); 237 238 var retValue = toJavaIdentifier( requireNotEmptyArgument( suggestion, "suggestion" ) ); 239 240 while( SourceVersion.isKeyword( retValue ) || !m_AllocatedNames.add( retValue ) ) 241 { 242 retValue += "_"; 243 } 244 245 final var replaced = m_TagToName.put( tag, retValue ); 246 if( nonNull( replaced ) ) 247 { 248 m_TagToName.put( tag, replaced ); // Put things back as they were! 249 throw new ValidationException( "tag '%s' cannot be used for both '%s' and '%s'".formatted( tag, replaced, suggestion ) ); 250 } 251 252 //---* Done *---------------------------------------------------------- 253 return retValue; 254 } // newName() 255} 256// class NameAllocator 257 258/* 259 * End of File 260 */