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