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.i18n.I18n;
031    import org.apache.directory.shared.ldap.entry.client.ClientStringValue;
032    import org.apache.directory.shared.ldap.util.Position;
033    import org.apache.directory.shared.ldap.util.StringTools;
034    
035    
036    /**
037     * A fast LDAP DN parser that handles only simple DNs. If the DN contains
038     * any special character an {@link TooComplexException} is thrown.
039     *
040     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
041     * @version $Rev: 664290 $, $Date: 2008-06-07 08:28:06 +0200 (Sa, 07 Jun 2008) $
042     */
043    public enum FastDnParser implements NameParser
044    {
045        INSTANCE;
046    
047        /**
048         * Gets the name parser singleton instance.
049         * 
050         * @return the name parser
051         */
052        public static NameParser getNameParser()
053        {
054            return INSTANCE;
055        }
056    
057    
058        /* (non-Javadoc)
059         * @see javax.naming.NameParser#parse(java.lang.String)
060         */
061        public Name parse( String name ) throws NamingException
062        {
063            DN dn = new DN();
064            parseDn( name, dn );
065            return dn;
066        }
067    
068    
069        /**
070         * Parses the given name string and fills the given DN object.
071         * 
072         * @param name the name to parse
073         * @param dn the DN to fill
074         * 
075         * @throws InvalidNameException the invalid name exception
076         */
077        public void parseDn( String name, DN dn ) throws InvalidNameException
078        {
079            parseDn(name, dn.rdns);
080            dn.setUpName( name );
081            dn.normalizeInternal();
082        }
083        
084        void parseDn( String name, List<RDN> rdns ) throws InvalidNameException
085        {
086            if ( ( name == null ) || ( name.trim().length() == 0 ) )
087            {
088                // We have an empty DN, just get out of the function.
089                return;
090            }
091    
092            Position pos = new Position();
093            pos.start = 0;
094            pos.length = name.length();
095    
096            while ( true )
097            {
098                RDN rdn = new RDN();
099                parseRdnInternal( name, pos, rdn );
100                rdns.add( rdn );
101    
102                if ( !hasMoreChars( pos ) )
103                {
104                    // end of line reached
105                    break;
106                }
107                char c = nextChar( name, pos, true );
108                switch ( c )
109                {
110                    case ',':
111                    case ';':
112                        // another RDN to parse
113                        break;
114    
115                    default:
116                        throw new InvalidNameException( I18n.err( I18n.ERR_04192, c, pos.start) );
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( I18n.err( I18n.ERR_04193 ) );
135            }
136            if( rdn == null )
137            {
138                throw new InvalidNameException( I18n.err( I18n.ERR_04194 ) );
139            }
140    
141            Position pos = new Position();
142            pos.start = 0;
143            pos.length = name.length();
144    
145            parseRdnInternal( name, pos, rdn );
146        }
147    
148    
149        private void parseRdnInternal( String name, Position pos, RDN rdn ) throws InvalidNameException
150        {
151            int rdnStart = pos.start;
152    
153            // SPACE*
154            matchSpaces( name, pos );
155    
156            // attributeType: ALPHA (ALPHA|DIGIT|HYPEN) | NUMERICOID
157            String type = matchAttributeType( name, pos );
158    
159            // SPACE*
160            matchSpaces( name, pos );
161    
162            // EQUALS
163            matchEquals( name, pos );
164    
165            // SPACE*
166            matchSpaces( name, pos );
167    
168            // here we only match "simple" values
169            // stops at \ + # " -> Too Complex Exception
170            String upValue = matchValue( name, pos );
171            String value = StringTools.trimRight( upValue );
172            // TODO: trim, normalize, etc
173    
174            // SPACE*
175            matchSpaces( name, pos );
176    
177            String upName = name.substring( rdnStart, pos.start );
178    
179            AVA ava = new AVA( type, type, new ClientStringValue( upValue ),
180                new ClientStringValue( value ), upName );
181            rdn.addAttributeTypeAndValue( ava );
182    
183            rdn.setUpName( upName );
184            rdn.normalize();
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( I18n.err( I18n.ERR_04195, c, pos.start) );
295            }
296        }
297    
298    
299        /**
300         * Matches attribute type descr.
301         * 
302         * @param name the name
303         * @param pos the pos
304         * 
305         * @return the attribute type descr
306         * 
307         * @throws InvalidNameException the invalid name exception
308         */
309        private String matchAttributeTypeDescr( String name, Position pos ) throws InvalidNameException
310        {
311            StringBuilder descr = new StringBuilder();
312            while ( hasMoreChars( pos ) )
313            {
314                char c = nextChar( name, pos, true );
315                switch ( c )
316                {
317                    case 'A':
318                    case 'B':
319                    case 'C':
320                    case 'D':
321                    case 'E':
322                    case 'F':
323                    case 'G':
324                    case 'H':
325                    case 'I':
326                    case 'J':
327                    case 'K':
328                    case 'L':
329                    case 'M':
330                    case 'N':
331                    case 'O':
332                    case 'P':
333                    case 'Q':
334                    case 'R':
335                    case 'S':
336                    case 'T':
337                    case 'U':
338                    case 'V':
339                    case 'W':
340                    case 'X':
341                    case 'Y':
342                    case 'Z':
343                    case 'a':
344                    case 'b':
345                    case 'c':
346                    case 'd':
347                    case 'e':
348                    case 'f':
349                    case 'g':
350                    case 'h':
351                    case 'i':
352                    case 'j':
353                    case 'k':
354                    case 'l':
355                    case 'm':
356                    case 'n':
357                    case 'o':
358                    case 'p':
359                    case 'q':
360                    case 'r':
361                    case 's':
362                    case 't':
363                    case 'u':
364                    case 'v':
365                    case 'w':
366                    case 'x':
367                    case 'y':
368                    case 'z':
369                    case '0':
370                    case '1':
371                    case '2':
372                    case '3':
373                    case '4':
374                    case '5':
375                    case '6':
376                    case '7':
377                    case '8':
378                    case '9':
379                    case '-':
380                        descr.append( c );
381                        break;
382    
383                    case ' ':
384                    case '=':
385                        pos.start--;
386                        return descr.toString();
387    
388                    case '.':
389                        // occurs for RDNs of form "oid.1.2.3=test"
390                        throw new TooComplexException();
391    
392                    default:
393                        // error
394                        throw new InvalidNameException( I18n.err( I18n.ERR_04196, c, pos.start ) );
395                }
396            }
397            return descr.toString();
398        }
399    
400    
401        /**
402         * Matches attribute type numeric OID.
403         * 
404         * @param name the name
405         * @param pos the pos
406         * 
407         * @return the attribute type OID
408         * 
409         * @throws InvalidNameException the invalid name exception
410         */
411        private String matchAttributeTypeNumericOid( String name, Position pos ) throws InvalidNameException
412        {
413            StringBuilder numericOid = new StringBuilder();
414            int dotCount = 0;
415            while ( true )
416            {
417                char c = nextChar( name, pos, true );
418                switch ( c )
419                {
420                    case '0':
421                        // leading '0', no other digit may follow!
422                        numericOid.append( c );
423                        c = nextChar( name, pos, true );
424                        switch ( c )
425                        {
426                            case '.':
427                                numericOid.append( c );
428                                dotCount++;
429                                break;
430                            case ' ':
431                            case '=':
432                                pos.start--;
433                                break;
434                            default:
435                                throw new InvalidNameException( I18n.err( I18n.ERR_04197, c, pos.start ) );
436                        }
437                        break;
438    
439                    case '1':
440                    case '2':
441                    case '3':
442                    case '4':
443                    case '5':
444                    case '6':
445                    case '7':
446                    case '8':
447                    case '9':
448                        numericOid.append( c );
449                        boolean inInnerLoop = true;
450                        while ( inInnerLoop )
451                        {
452                            c = nextChar( name, pos, true );
453                            switch ( c )
454                            {
455                                case ' ':
456                                case '=':
457                                    inInnerLoop = false;
458                                    pos.start--;
459                                    break;
460                                case '.':
461                                    inInnerLoop = false;
462                                    dotCount++;
463                                    // no break!
464                                case '0':
465                                case '1':
466                                case '2':
467                                case '3':
468                                case '4':
469                                case '5':
470                                case '6':
471                                case '7':
472                                case '8':
473                                case '9':
474                                    numericOid.append( c );
475                                    break;
476                                default:
477                                    throw new InvalidNameException( I18n.err( I18n.ERR_04197, c, pos.start ) );
478                            }
479                        }
480                        break;
481                    case ' ':
482                    case '=':
483                        pos.start--;
484                        if ( dotCount > 0 )
485                        {
486                            return numericOid.toString();
487                        }
488                        else
489                        {
490                            throw new InvalidNameException( I18n.err( I18n.ERR_04198 ) );
491                        }
492                    default:
493                        throw new InvalidNameException( I18n.err( I18n.ERR_04199, c, pos.start ) );
494                }
495            }
496        }
497    
498    
499        /**
500         * Matches the equals character.
501         * 
502         * @param name the name
503         * @param pos the pos
504         * 
505         * @throws InvalidNameException the invalid name exception
506         */
507        private void matchEquals( String name, Position pos ) throws InvalidNameException
508        {
509            char c = nextChar( name, pos, true );
510            if ( c != '=' )
511            {
512                throw new InvalidNameException( I18n.err( I18n.ERR_04200, c, pos.start ) );
513            }
514        }
515    
516    
517        /**
518         * Matches the assertion value. This method only handles simple values.
519         * If we find any special character (BACKSLASH, PLUS, SHARP or DQUOTE),
520         * a TooComplexException will be thrown.
521         * 
522         * @param name the name
523         * @param pos the pos
524         * 
525         * @return the string
526         * 
527         * @throws InvalidNameException the invalid name exception
528         */
529        private String matchValue( String name, Position pos ) throws InvalidNameException
530        {
531            StringBuilder value = new StringBuilder();
532            int numTrailingSpaces = 0;
533            while ( true )
534            {
535                if ( !hasMoreChars( pos ) )
536                {
537                    pos.start -= numTrailingSpaces;
538                    return value.substring( 0, value.length() - numTrailingSpaces );
539                }
540                char c = nextChar( name, pos, true );
541                switch ( c )
542                {
543                    case '\\':
544                    case '+':
545                    case '#':
546                    case '"':
547                        throw new TooComplexException();
548                    case ',':
549                    case ';':
550                        pos.start--;
551                        pos.start -= numTrailingSpaces;
552                        return value.substring( 0, value.length() - numTrailingSpaces );
553                    case ' ':
554                        numTrailingSpaces++;
555                        value.append( c );
556                        break;
557                    default:
558                        numTrailingSpaces = 0;
559                        value.append( c );
560                }
561            }
562        }
563    
564    
565        /**
566         * Gets the next character.
567         * 
568         * @param name the name
569         * @param pos the pos
570         * @param increment true to increment the position
571         * 
572         * @return the character
573         * @throws InvalidNameException If no more characters are available
574         */
575        private char nextChar( String name, Position pos, boolean increment ) throws InvalidNameException
576        {
577            if ( !hasMoreChars( pos ) )
578            {
579                throw new InvalidNameException( I18n.err( I18n.ERR_04201, pos.start ) );
580            }
581            char c = name.charAt( pos.start );
582            if ( increment )
583            {
584                pos.start++;
585            }
586            return c;
587        }
588    
589    
590        /**
591         * Checks if there are more characters.
592         * 
593         * @param pos the pos
594         * 
595         * @return true, if more characters are available
596         */
597        private boolean hasMoreChars( Position pos )
598        {
599            return pos.start < pos.length;
600        }
601    }