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    }