001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 *
019 */
020 package org.apache.directory.shared.ldap.name;
021
022
023 import java.util.List;
024
025 import javax.naming.InvalidNameException;
026 import javax.naming.Name;
027 import javax.naming.NameParser;
028 import javax.naming.NamingException;
029
030 import org.apache.directory.shared.ldap.entry.client.ClientStringValue;
031 import org.apache.directory.shared.ldap.util.Position;
032 import org.apache.directory.shared.ldap.util.StringTools;
033
034
035 /**
036 * A fast LDAP DN parser that handles only simple DNs. If the DN contains
037 * any special character an {@link TooComplexException} is thrown.
038 *
039 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
040 * @version $Rev: 664290 $, $Date: 2008-06-07 08:28:06 +0200 (Sa, 07 Jun 2008) $
041 */
042 public enum FastLdapDnParser implements NameParser
043 {
044 INSTANCE;
045
046 /**
047 * Gets the name parser singleton instance.
048 *
049 * @return the name parser
050 */
051 public static NameParser getNameParser()
052 {
053 return INSTANCE;
054 }
055
056
057 /* (non-Javadoc)
058 * @see javax.naming.NameParser#parse(java.lang.String)
059 */
060 public Name parse( String name ) throws NamingException
061 {
062 LdapDN dn = new LdapDN();
063 parseDn( name, dn );
064 return dn;
065 }
066
067
068 /**
069 * Parses the given name string and fills the given LdapDN object.
070 *
071 * @param name the name to parse
072 * @param dn the LdapDN to fill
073 *
074 * @throws InvalidNameException the invalid name exception
075 */
076 public void parseDn( String name, LdapDN dn ) throws InvalidNameException
077 {
078 parseDn(name, dn.rdns);
079 dn.setUpName( name );
080 dn.normalizeInternal();
081 }
082
083 void parseDn( String name, List<Rdn> rdns ) throws InvalidNameException
084 {
085 if ( name == null || name.trim().length() == 0 )
086 {
087 // We have an empty DN, just get out of the function.
088 return;
089 }
090
091 Position pos = new Position();
092 pos.start = 0;
093 pos.length = name.length();
094
095 while ( true )
096 {
097 Rdn rdn = new Rdn();
098 parseRdnInternal( name, pos, rdn );
099 rdns.add( rdn );
100
101 if ( !hasMoreChars( pos ) )
102 {
103 // end of line reached
104 break;
105 }
106 char c = nextChar( name, pos, true );
107 switch ( c )
108 {
109 case ',':
110 case ';':
111 // another RDN to parse
112 break;
113
114 default:
115 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
116 + ". Excpected ',' or ';'." );
117 }
118 }
119 }
120
121
122 /**
123 * Parses the given name string and fills the given Rdn object.
124 *
125 * @param name the name to parse
126 * @param rdn the RDN to fill
127 *
128 * @throws InvalidNameException the invalid name exception
129 */
130 public void parseRdn( String name, Rdn rdn ) throws InvalidNameException
131 {
132 if ( name == null || name.length() == 0 )
133 {
134 throw new InvalidNameException( "RDN must not be empty" );
135 }
136
137 Position pos = new Position();
138 pos.start = 0;
139 pos.length = name.length();
140
141 parseRdnInternal( name, pos, rdn );
142
143 if ( !hasMoreChars( pos ) )
144 {
145 throw new InvalidNameException( "Expected no more characters at position " + pos.start );
146 }
147 }
148
149
150 private void parseRdnInternal( String name, Position pos, Rdn rdn ) throws InvalidNameException
151 {
152 int rdnStart = pos.start;
153
154 // SPACE*
155 matchSpaces( name, pos );
156
157 // attributeType: ALPHA (ALPHA|DIGIT|HYPEN) | NUMERICOID
158 String type = matchAttributeType( name, pos );
159
160 // SPACE*
161 matchSpaces( name, pos );
162
163 // EQUALS
164 matchEquals( name, pos );
165
166 // SPACE*
167 matchSpaces( name, pos );
168
169 // here we only match "simple" values
170 // stops at \ + # " -> Too Complex Exception
171 String upValue = matchValue( name, pos );
172 String value = StringTools.trimRight( upValue );
173 // TODO: trim, normalize, etc
174
175 // SPACE*
176 matchSpaces( name, pos );
177
178 rdn.addAttributeTypeAndValue( type, type,
179 new ClientStringValue( upValue ),
180 new ClientStringValue( value ) );
181
182 rdn.setUpName( name.substring( rdnStart, pos.start ) );
183 rdn.normalize();
184
185 }
186
187
188 /**
189 * Matches and forgets optional spaces.
190 *
191 * @param name the name
192 * @param pos the pos
193 * @throws InvalidNameException
194 */
195 private void matchSpaces( String name, Position pos ) throws InvalidNameException
196 {
197 while ( hasMoreChars( pos ) )
198 {
199 char c = nextChar( name, pos, true );
200 if ( c != ' ' )
201 {
202 pos.start--;
203 break;
204 }
205 }
206 }
207
208
209 /**
210 * Matches attribute type.
211 *
212 * @param name the name
213 * @param pos the pos
214 *
215 * @return the matched attribute type
216 *
217 * @throws InvalidNameException the invalid name exception
218 */
219 private String matchAttributeType( String name, Position pos ) throws InvalidNameException
220 {
221 char c = nextChar( name, pos, false );
222 switch ( c )
223 {
224 case 'A':
225 case 'B':
226 case 'C':
227 case 'D':
228 case 'E':
229 case 'F':
230 case 'G':
231 case 'H':
232 case 'I':
233 case 'J':
234 case 'K':
235 case 'L':
236 case 'M':
237 case 'N':
238 case 'O':
239 case 'P':
240 case 'Q':
241 case 'R':
242 case 'S':
243 case 'T':
244 case 'U':
245 case 'V':
246 case 'W':
247 case 'X':
248 case 'Y':
249 case 'Z':
250 case 'a':
251 case 'b':
252 case 'c':
253 case 'd':
254 case 'e':
255 case 'f':
256 case 'g':
257 case 'h':
258 case 'i':
259 case 'j':
260 case 'k':
261 case 'l':
262 case 'm':
263 case 'n':
264 case 'o':
265 case 'p':
266 case 'q':
267 case 'r':
268 case 's':
269 case 't':
270 case 'u':
271 case 'v':
272 case 'w':
273 case 'x':
274 case 'y':
275 case 'z':
276 // descr
277 return matchAttributeTypeDescr( name, pos );
278
279 case '0':
280 case '1':
281 case '2':
282 case '3':
283 case '4':
284 case '5':
285 case '6':
286 case '7':
287 case '8':
288 case '9':
289 // numericoid
290 return matchAttributeTypeNumericOid( name, pos );
291
292 default:
293 // error
294 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
295 + ". Excpected start of attributeType." );
296 }
297 }
298
299
300 /**
301 * Matches attribute type descr.
302 *
303 * @param name the name
304 * @param pos the pos
305 *
306 * @return the attribute type descr
307 *
308 * @throws InvalidNameException the invalid name exception
309 */
310 private String matchAttributeTypeDescr( String name, Position pos ) throws InvalidNameException
311 {
312 StringBuilder descr = new StringBuilder();
313 while ( hasMoreChars( pos ) )
314 {
315 char c = nextChar( name, pos, true );
316 switch ( c )
317 {
318 case 'A':
319 case 'B':
320 case 'C':
321 case 'D':
322 case 'E':
323 case 'F':
324 case 'G':
325 case 'H':
326 case 'I':
327 case 'J':
328 case 'K':
329 case 'L':
330 case 'M':
331 case 'N':
332 case 'O':
333 case 'P':
334 case 'Q':
335 case 'R':
336 case 'S':
337 case 'T':
338 case 'U':
339 case 'V':
340 case 'W':
341 case 'X':
342 case 'Y':
343 case 'Z':
344 case 'a':
345 case 'b':
346 case 'c':
347 case 'd':
348 case 'e':
349 case 'f':
350 case 'g':
351 case 'h':
352 case 'i':
353 case 'j':
354 case 'k':
355 case 'l':
356 case 'm':
357 case 'n':
358 case 'o':
359 case 'p':
360 case 'q':
361 case 'r':
362 case 's':
363 case 't':
364 case 'u':
365 case 'v':
366 case 'w':
367 case 'x':
368 case 'y':
369 case 'z':
370 case '0':
371 case '1':
372 case '2':
373 case '3':
374 case '4':
375 case '5':
376 case '6':
377 case '7':
378 case '8':
379 case '9':
380 case '-':
381 descr.append( c );
382 break;
383
384 case ' ':
385 case '=':
386 pos.start--;
387 return descr.toString();
388
389 case '.':
390 // occurs for RDNs of form "oid.1.2.3=test"
391 throw new TooComplexException();
392
393 default:
394 // error
395 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
396 + ". Excpected start of attributeType descr." );
397 }
398 }
399 return descr.toString();
400 }
401
402
403 /**
404 * Matches attribute type numeric OID.
405 *
406 * @param name the name
407 * @param pos the pos
408 *
409 * @return the attribute type OID
410 *
411 * @throws InvalidNameException the invalid name exception
412 */
413 private String matchAttributeTypeNumericOid( String name, Position pos ) throws InvalidNameException
414 {
415 StringBuilder numericOid = new StringBuilder();
416 int dotCount = 0;
417 while ( true )
418 {
419 char c = nextChar( name, pos, true );
420 switch ( c )
421 {
422 case '0':
423 // leading '0', no other digit may follow!
424 numericOid.append( c );
425 c = nextChar( name, pos, true );
426 switch ( c )
427 {
428 case '.':
429 numericOid.append( c );
430 dotCount++;
431 break;
432 case ' ':
433 case '=':
434 pos.start--;
435 break;
436 default:
437 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
438 + ". Excpected numericoid." );
439 }
440 break;
441
442 case '1':
443 case '2':
444 case '3':
445 case '4':
446 case '5':
447 case '6':
448 case '7':
449 case '8':
450 case '9':
451 numericOid.append( c );
452 boolean inInnerLoop = true;
453 while ( inInnerLoop )
454 {
455 c = nextChar( name, pos, true );
456 switch ( c )
457 {
458 case ' ':
459 case '=':
460 inInnerLoop = false;
461 pos.start--;
462 break;
463 case '.':
464 inInnerLoop = false;
465 dotCount++;
466 // no break!
467 case '0':
468 case '1':
469 case '2':
470 case '3':
471 case '4':
472 case '5':
473 case '6':
474 case '7':
475 case '8':
476 case '9':
477 numericOid.append( c );
478 break;
479 default:
480 throw new InvalidNameException( "Unexpected character '" + c + "' at position "
481 + pos.start + ". Excpected numericoid." );
482 }
483 }
484 break;
485 case ' ':
486 case '=':
487 pos.start--;
488 if ( dotCount > 0 )
489 {
490 return numericOid.toString();
491 }
492 else
493 {
494 throw new InvalidNameException( "Numeric OID must contain at least one dot." );
495 }
496 default:
497 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
498 + ". Excpected start of attributeType numericoid." );
499 }
500 }
501 }
502
503
504 /**
505 * Matches the equals character.
506 *
507 * @param name the name
508 * @param pos the pos
509 *
510 * @throws InvalidNameException the invalid name exception
511 */
512 private void matchEquals( String name, Position pos ) throws InvalidNameException
513 {
514 char c = nextChar( name, pos, true );
515 if ( c != '=' )
516 {
517 throw new InvalidNameException( "Unexpected character '" + c + "' at position " + pos.start
518 + ". Excpected EQUALS '='." );
519 }
520 }
521
522
523 /**
524 * Matches the assertion value. This method only handles simple values.
525 * If we find any special character (BACKSLASH, PLUS, SHARP or DQUOTE),
526 * a TooComplexException will be thrown.
527 *
528 * @param name the name
529 * @param pos the pos
530 *
531 * @return the string
532 *
533 * @throws InvalidNameException the invalid name exception
534 */
535 private String matchValue( String name, Position pos ) throws InvalidNameException
536 {
537 StringBuilder value = new StringBuilder();
538 int numTrailingSpaces = 0;
539 while ( true )
540 {
541 if ( !hasMoreChars( pos ) )
542 {
543 pos.start -= numTrailingSpaces;
544 return value.substring( 0, value.length() - numTrailingSpaces );
545 }
546 char c = nextChar( name, pos, true );
547 switch ( c )
548 {
549 case '\\':
550 case '+':
551 case '#':
552 case '"':
553 throw new TooComplexException();
554 case ',':
555 case ';':
556 pos.start--;
557 pos.start -= numTrailingSpaces;
558 return value.substring( 0, value.length() - numTrailingSpaces );
559 case ' ':
560 numTrailingSpaces++;
561 value.append( c );
562 break;
563 default:
564 numTrailingSpaces = 0;
565 value.append( c );
566 }
567 }
568 }
569
570
571 /**
572 * Gets the next character.
573 *
574 * @param name the name
575 * @param pos the pos
576 * @param increment true to increment the position
577 *
578 * @return the character
579 * @throws InvalidNameException If no more characters are available
580 */
581 private char nextChar( String name, Position pos, boolean increment ) throws InvalidNameException
582 {
583 if ( !hasMoreChars( pos ) )
584 {
585 throw new InvalidNameException( "No more characters available at position " + pos.start );
586 }
587 char c = name.charAt( pos.start );
588 if ( increment )
589 {
590 pos.start++;
591 }
592 return c;
593 }
594
595
596 /**
597 * Checks if there are more characters.
598 *
599 * @param pos the pos
600 *
601 * @return true, if more characters are available
602 */
603 private boolean hasMoreChars( Position pos )
604 {
605 return pos.start < pos.length;
606 }
607 }