1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 package org.jomc.model;
32
33 import java.io.Serializable;
34 import java.lang.ref.Reference;
35 import java.lang.ref.SoftReference;
36 import java.text.MessageFormat;
37 import java.text.ParseException;
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Map;
43 import java.util.ResourceBundle;
44
45
46
47
48
49
50
51
52
53
54
55
56
57 public final class JavaIdentifier implements CharSequence, Serializable
58 {
59
60
61
62
63
64
65
66
67
68 public static enum NormalizationMode
69 {
70
71
72 CAMEL_CASE,
73
74 LOWER_CASE,
75
76 UPPER_CASE,
77
78
79
80
81
82
83
84
85 CONSTANT_NAME_CONVENTION,
86
87
88
89
90
91
92
93
94 METHOD_NAME_CONVENTION,
95
96
97
98
99
100
101
102
103
104
105
106
107 VARIABLE_NAME_CONVENTION
108
109 }
110
111
112
113
114
115 private String identifier;
116
117
118 private static volatile Reference<Map<CacheKey, JavaIdentifier>> cache;
119
120
121 private static final long serialVersionUID = 7600377999055800720L;
122
123
124 private static final int UNDERSCORE_CODEPOINT = Character.codePointAt( "_", 0 );
125
126
127 private JavaIdentifier()
128 {
129 super();
130 }
131
132
133
134
135
136
137 public int length()
138 {
139 return this.identifier.length();
140 }
141
142
143
144
145
146
147
148
149
150
151 public char charAt( final int index )
152 {
153 return this.identifier.charAt( index );
154 }
155
156
157
158
159
160
161
162
163
164
165
166
167 public CharSequence subSequence( final int start, final int end )
168 {
169 return this.identifier.subSequence( start, end );
170 }
171
172
173
174
175
176
177
178 @Override
179 public String toString()
180 {
181 return this.identifier;
182 }
183
184
185
186
187
188
189 @Override
190 public int hashCode()
191 {
192 return this.identifier.hashCode();
193 }
194
195
196
197
198
199
200
201
202
203 @Override
204 public boolean equals( final Object o )
205 {
206 boolean equal = o == this;
207
208 if ( !equal && o instanceof JavaIdentifier )
209 {
210 equal = this.toString().equals( o.toString() );
211 }
212
213 return equal;
214 }
215
216
217
218
219
220
221
222
223
224
225
226
227 public static JavaIdentifier normalize( final String text, final NormalizationMode mode ) throws ParseException
228 {
229 if ( text == null )
230 {
231 throw new NullPointerException( "text" );
232 }
233 if ( mode == null )
234 {
235 throw new NullPointerException( "mode" );
236 }
237
238 return parse( text, mode, false );
239 }
240
241
242
243
244
245
246
247
248
249
250
251
252
253 public static JavaIdentifier parse( final String text ) throws ParseException
254 {
255 if ( text == null )
256 {
257 throw new NullPointerException( "text" );
258 }
259
260 return parse( text, null, false );
261 }
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277 public static JavaIdentifier valueOf( final String text ) throws IllegalArgumentException
278 {
279 if ( text == null )
280 {
281 throw new NullPointerException( "text" );
282 }
283
284 try
285 {
286 return parse( text, null, true );
287 }
288 catch ( final ParseException e )
289 {
290 throw new AssertionError( e );
291 }
292 }
293
294 private static JavaIdentifier parse( final String text, final NormalizationMode mode,
295 final boolean runtimeException )
296 throws ParseException
297 {
298 Map<CacheKey, JavaIdentifier> map = cache == null ? null : cache.get();
299
300 if ( map == null )
301 {
302 map = new HashMap<CacheKey, JavaIdentifier>( 128 );
303 cache = new SoftReference<Map<CacheKey, JavaIdentifier>>( map );
304 }
305
306 synchronized ( map )
307 {
308 final CacheKey key = new CacheKey( text, mode );
309 JavaIdentifier javaIdentifier = map.get( key );
310
311 if ( javaIdentifier == null )
312 {
313 javaIdentifier = new JavaIdentifier();
314 parseIdentifier( javaIdentifier, text, mode, runtimeException );
315
316 if ( mode != null )
317 {
318 final CacheKey normalizedKey = new CacheKey( javaIdentifier.toString(), mode );
319 final JavaIdentifier normalizedInstance = map.get( normalizedKey );
320
321 if ( normalizedInstance != null )
322 {
323 map.put( key, normalizedInstance );
324 javaIdentifier = normalizedInstance;
325 }
326 else
327 {
328 map.put( key, javaIdentifier );
329 map.put( normalizedKey, javaIdentifier );
330 }
331 }
332 else
333 {
334 map.put( key, javaIdentifier );
335 }
336 }
337
338 return javaIdentifier;
339 }
340 }
341
342 private static void parseIdentifier( final JavaIdentifier t, final String text, final NormalizationMode mode,
343 final boolean runtimeException )
344 throws ParseException
345 {
346 if ( text.length() <= 0 )
347 {
348 if ( runtimeException )
349 {
350 throw new IllegalArgumentException( getMessage( "invalidEmptyString" ) );
351 }
352 else
353 {
354 throw new ParseException( getMessage( "invalidEmptyString" ), 0 );
355 }
356 }
357
358 final StringBuilder identifierBuilder = new StringBuilder( text.length() );
359 final List<Integer> retainedIndices = new ArrayList<Integer>( text.length() );
360 boolean start_of_word = true;
361 int words = 0;
362
363 for ( int i = 0, j = 1, s0 = text.length(), last_codepoint = -1; i < s0; i++, j++ )
364 {
365 if ( !isWordSeparator( text.codePointAt( i ), mode, identifierBuilder.length() <= 0 ) )
366 {
367 if ( mode != null )
368 {
369 switch ( mode )
370 {
371 case CAMEL_CASE:
372 if ( start_of_word )
373 {
374 identifierBuilder.append( Character.toUpperCase( text.charAt( i ) ) );
375 }
376 else if ( last_codepoint > -1 && j < s0
377 && isCamelCase( last_codepoint, text.codePointAt( i ), text.codePointAt( j ) ) )
378 {
379 identifierBuilder.append( text.charAt( i ) );
380 retainedIndices.add( identifierBuilder.length() - 1 );
381 }
382 else
383 {
384 identifierBuilder.append( Character.toLowerCase( text.charAt( i ) ) );
385 }
386 break;
387
388 case LOWER_CASE:
389 if ( start_of_word && last_codepoint > -1 && last_codepoint != UNDERSCORE_CODEPOINT )
390 {
391 identifierBuilder.append( Character.toChars( UNDERSCORE_CODEPOINT ) );
392 }
393
394 identifierBuilder.append( Character.toLowerCase( text.charAt( i ) ) );
395 break;
396
397 case UPPER_CASE:
398 case CONSTANT_NAME_CONVENTION:
399 if ( start_of_word && last_codepoint > -1 && last_codepoint != UNDERSCORE_CODEPOINT )
400 {
401 identifierBuilder.append( Character.toChars( UNDERSCORE_CODEPOINT ) );
402 }
403
404 identifierBuilder.append( Character.toUpperCase( text.charAt( i ) ) );
405 break;
406
407 case VARIABLE_NAME_CONVENTION:
408 case METHOD_NAME_CONVENTION:
409 if ( start_of_word )
410 {
411 identifierBuilder.append( words == 0 ? Character.toLowerCase( text.charAt( i ) )
412 : Character.toUpperCase( text.charAt( i ) ) );
413
414 }
415 else if ( last_codepoint > -1 && j < s0
416 && isCamelCase( last_codepoint, text.codePointAt( i ), text.codePointAt( j ) ) )
417 {
418 identifierBuilder.append( text.charAt( i ) );
419 retainedIndices.add( identifierBuilder.length() - 1 );
420 }
421 else
422 {
423 identifierBuilder.append( Character.toLowerCase( text.charAt( i ) ) );
424 }
425 break;
426
427 default:
428 throw new AssertionError( mode );
429
430 }
431 }
432 else
433 {
434 identifierBuilder.append( text.charAt( i ) );
435 }
436
437 last_codepoint = identifierBuilder.codePointAt( identifierBuilder.length() - 1 );
438 start_of_word = false;
439 }
440 else
441 {
442 if ( mode != null )
443 {
444 if ( !start_of_word )
445 {
446 start_of_word = true;
447 words++;
448 }
449 }
450 else if ( runtimeException )
451 {
452 throw new IllegalArgumentException( getMessage( "invalidCharacter", text, text.charAt( i ), i ) );
453 }
454 else
455 {
456 throw new ParseException( getMessage( "invalidCharacter", text, text.charAt( i ), i ), i );
457 }
458 }
459 }
460
461 if ( words > 0 )
462 {
463
464 toLowerCase( identifierBuilder, retainedIndices );
465 }
466
467 t.identifier = identifierBuilder.toString();
468
469 if ( t.identifier.length() <= 0 )
470 {
471 if ( runtimeException )
472 {
473 throw new IllegalArgumentException( getMessage( "invalidCharacters", text ) );
474 }
475 else
476 {
477 throw new ParseException( getMessage( "invalidCharacters", text ), 0 );
478 }
479 }
480
481 if ( JavaLanguage.KEYWORDS.contains( t.identifier )
482 || JavaLanguage.BOOLEAN_LITERALS.contains( t.identifier )
483 || JavaLanguage.NULL_LITERAL.equals( t.identifier ) )
484 {
485 if ( mode != null )
486 {
487 t.identifier = "_" + t.identifier;
488 }
489 else if ( runtimeException )
490 {
491 throw new IllegalArgumentException( getMessage( "invalidWord", text, t.identifier,
492 text.indexOf( t.identifier ) ) );
493
494 }
495 else
496 {
497 throw new ParseException( getMessage( "invalidWord", text, t.identifier, text.indexOf( t.identifier ) ),
498 text.indexOf( t.identifier ) );
499
500 }
501 }
502 }
503
504 private static boolean isWordSeparator( final int codePoint, final NormalizationMode mode, final boolean first )
505 {
506 return !( ( first ? Character.isJavaIdentifierStart( codePoint ) : Character.isJavaIdentifierPart( codePoint ) )
507 && ( mode != null ? Character.isLetterOrDigit( codePoint ) : true ) );
508
509 }
510
511 private static boolean isCamelCase( final int left, final int middle, final int right )
512 {
513 return Character.isLowerCase( left ) && Character.isUpperCase( middle ) && Character.isLowerCase( right );
514 }
515
516 private static void toLowerCase( final StringBuilder stringBuilder, final List<Integer> indices )
517 {
518 for ( int i = 0, s0 = indices.size(); i < s0; i++ )
519 {
520 final int index = indices.get( i );
521 final int cp = Character.toLowerCase( stringBuilder.codePointAt( index ) );
522 stringBuilder.replace( index, index + 1, String.valueOf( Character.toChars( cp ) ) );
523 }
524 }
525
526 private static String getMessage( final String key, final Object... args )
527 {
528 return MessageFormat.format( ResourceBundle.getBundle(
529 JavaIdentifier.class.getName().replace( '.', '/' ), Locale.getDefault() ).
530 getString( key ), args );
531
532 }
533
534 private static final class CacheKey
535 {
536
537 private final String text;
538
539 private final NormalizationMode mode;
540
541 private CacheKey( final String text, final NormalizationMode mode )
542 {
543 super();
544 this.text = text;
545 this.mode = mode;
546 }
547
548 @Override
549 public int hashCode()
550 {
551 int hc = 23;
552 hc = 37 * hc + this.text.hashCode();
553 hc = 37 * hc + ( this.mode == null ? 0 : this.mode.hashCode() );
554 return hc;
555 }
556
557 @Override
558 public boolean equals( final Object o )
559 {
560 boolean equal = o == this;
561
562 if ( !equal && o instanceof CacheKey )
563 {
564 final CacheKey that = (CacheKey) o;
565 equal = this.mode == that.mode && this.text.equals( that.text );
566 }
567
568 return equal;
569 }
570
571 }
572
573 }