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