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>  &#64;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     *  (&quot;_&quot;). 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 */