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 */
020package org.apache.directory.api.ldap.model.entry;
021
022
023import java.text.ParseException;
024import java.util.Arrays;
025import java.util.Iterator;
026
027import javax.naming.NamingEnumeration;
028import javax.naming.NamingException;
029import javax.naming.directory.Attributes;
030import javax.naming.directory.BasicAttribute;
031import javax.naming.directory.BasicAttributes;
032
033import org.apache.directory.api.i18n.I18n;
034import org.apache.directory.api.ldap.model.exception.LdapException;
035import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeTypeException;
036import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
037import org.apache.directory.api.ldap.model.name.Dn;
038import org.apache.directory.api.util.Chars;
039import org.apache.directory.api.util.Position;
040import org.apache.directory.api.util.Strings;
041
042
043/**
044 * A set of utility fuctions for working with Attributes.
045 * 
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public final class AttributeUtils
049{
050    private AttributeUtils()
051    {
052    }
053
054
055    /**
056     * Check if an attribute contains a value. The test is case insensitive,
057     * and the value is supposed to be a String. If the value is a byte[],
058     * then the case sensitivity is useless.
059     *
060     * @param attr The attribute to check
061     * @param value The value to look for
062     * @return true if the value is present in the attribute
063     */
064    public static boolean containsValueCaseIgnore( javax.naming.directory.Attribute attr, Object value )
065    {
066        // quick bypass test
067        if ( attr.contains( value ) )
068        {
069            return true;
070        }
071
072        try
073        {
074            if ( value instanceof String )
075            {
076                String strVal = ( String ) value;
077
078                NamingEnumeration<?> attrVals = attr.getAll();
079
080                while ( attrVals.hasMoreElements() )
081                {
082                    Object attrVal = attrVals.nextElement();
083
084                    if ( attrVal instanceof String && strVal.equalsIgnoreCase( ( String ) attrVal ) )
085                    {
086                        return true;
087                    }
088                }
089            }
090            else
091            {
092                byte[] valueBytes = ( byte[] ) value;
093
094                NamingEnumeration<?> attrVals = attr.getAll();
095
096                while ( attrVals.hasMoreElements() )
097                {
098                    Object attrVal = attrVals.nextElement();
099
100                    if ( attrVal instanceof byte[] && Arrays.equals( ( byte[] ) attrVal, valueBytes ) )
101                    {
102                        return true;
103                    }
104                }
105            }
106        }
107        catch ( NamingException ne )
108        {
109            return false;
110        }
111
112        return false;
113    }
114
115
116    /**
117     * Check if the attributes is a BasicAttributes, and if so, switch
118     * the case sensitivity to false to avoid tricky problems in the server.
119     * (Ldap attributeTypes are *always* case insensitive)
120     * 
121     * @param attributes The Attributes to check
122     * @return The modified Attributes
123     */
124    public static Attributes toCaseInsensitive( Attributes attributes )
125    {
126        if ( attributes == null )
127        {
128            return attributes;
129        }
130
131        if ( attributes instanceof BasicAttributes )
132        {
133            if ( attributes.isCaseIgnored() )
134            {
135                // Just do nothing if the Attributes is already case insensitive
136                return attributes;
137            }
138            else
139            {
140                // Ok, bad news : we have to create a new BasicAttributes
141                // which will be case insensitive
142                Attributes newAttrs = new BasicAttributes( true );
143
144                NamingEnumeration<?> attrs = attributes.getAll();
145
146                if ( attrs != null )
147                {
148                    // Iterate through the attributes now
149                    while ( attrs.hasMoreElements() )
150                    {
151                        newAttrs.put( ( javax.naming.directory.Attribute ) attrs.nextElement() );
152                    }
153                }
154
155                return newAttrs;
156            }
157        }
158        else
159        {
160            // we can safely return the attributes if it's not a BasicAttributes
161            return attributes;
162        }
163    }
164
165
166    /**
167     * Parse attribute's options :
168     * 
169     * <pre>
170     * options = *( ';' option )
171     * option = 1*keychar
172     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
173     * </pre>
174     * 
175     * @param str The parsed option
176     * @param pos The position in the parsed option string
177     * @exception ParseException The parsed option is invalid
178     */
179    private static void parseOptions( char[] str, Position pos ) throws ParseException
180    {
181        while ( Strings.isCharASCII( str, pos.start, ';' ) )
182        {
183            pos.start++;
184
185            // We have an option
186            if ( !Chars.isAlphaDigitMinus( str, pos.start ) )
187            {
188                // We must have at least one keychar
189                throw new ParseException( I18n.err( I18n.ERR_13201_EMPTY_OPTION_NOT_ALLOWED ), pos.start );
190            }
191
192            pos.start++;
193
194            while ( Chars.isAlphaDigitMinus( str, pos.start ) )
195            {
196                pos.start++;
197            }
198        }
199    }
200
201
202
203
204    /**
205     * Parse attribute's options :
206     * 
207     * <pre>
208     * options = *( ';' option )
209     * option = 1*keychar
210     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
211     * </pre>
212     * 
213     * @param bytes The parsed option
214     * @param pos The position in the parsed option bytes
215     * @exception ParseException The parsed option is invalid
216     */
217    private static void parseOptions( byte[] bytes, Position pos ) throws ParseException
218    {
219        while ( Strings.isCharASCII( bytes, pos.start, ';' ) )
220        {
221            pos.start++;
222
223            // We have an option
224            if ( !Chars.isAlphaDigitMinus( bytes, pos.start ) )
225            {
226                // We must have at least one keychar
227                throw new ParseException( I18n.err( I18n.ERR_13201_EMPTY_OPTION_NOT_ALLOWED ), pos.start );
228            }
229
230            pos.start++;
231
232            while ( Chars.isAlphaDigitMinus( bytes, pos.start ) )
233            {
234                pos.start++;
235            }
236        }
237    }
238
239
240    /**
241     * Parse a number :
242     * 
243     * <pre>
244     * number = '0' | '1'..'9' digits
245     * digits = '0'..'9'*
246     * </pre>
247     * 
248     * @param filter The number in the filter
249     * @param pos The position in the parsed filter string
250     * @return true if a number has been found
251     */
252    private static boolean parseNumber( char[] filter, Position pos )
253    {
254        char c = Strings.charAt( filter, pos.start );
255
256        switch ( c )
257        {
258            case '0':
259                // If we get a starting '0', we should get out
260                pos.start++;
261                return true;
262
263            case '1':
264            case '2':
265            case '3':
266            case '4':
267            case '5':
268            case '6':
269            case '7':
270            case '8':
271            case '9':
272                pos.start++;
273                break;
274
275            default:
276                // Not a number.
277                return false;
278        }
279
280        while ( Chars.isDigit( filter, pos.start ) )
281        {
282            pos.start++;
283        }
284
285        return true;
286    }
287
288
289    /**
290     * Parse a number :
291     * 
292     * <pre>
293     * number = '0' | '1'..'9' digits
294     * digits = '0'..'9'*
295     * </pre>
296     * 
297     * @param bytes The parsed number
298     * @param pos The position in the parsed number string
299     * @return true if a number has been found
300     */
301    private static boolean parseNumber( byte[] bytes, Position pos )
302    {
303        byte b = Strings.byteAt( bytes, pos.start );
304
305        switch ( b )
306        {
307            case '0':
308                // If we get a starting '0', we should get out
309                pos.start++;
310                return true;
311
312            case '1':
313            case '2':
314            case '3':
315            case '4':
316            case '5':
317            case '6':
318            case '7':
319            case '8':
320            case '9':
321                pos.start++;
322                break;
323
324            default:
325                // Not a number.
326                return false;
327        }
328
329        while ( Chars.isDigit( bytes, pos.start ) )
330        {
331            pos.start++;
332        }
333
334        return true;
335    }
336
337
338    /**
339     * Parse an OID.
340     *
341     * numericoid = number 1*( '.' number )
342     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
343     *
344     * @param str The OID to parse
345     * @param pos The current position in the string
346     * @throws ParseException If we don't have a valid OID
347     */
348    private static void parseOID( char[] str, Position pos ) throws ParseException
349    {
350        // We have an OID
351        parseNumber( str, pos );
352
353        // We must have at least one '.' number
354        if ( !Strings.isCharASCII( str, pos.start, '.' ) )
355        {
356            throw new ParseException( I18n.err( I18n.ERR_13221_INVALID_OID_MISSING_DOT ), pos.start );
357        }
358
359        pos.start++;
360
361        if ( !parseNumber( str, pos ) )
362        {
363            throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start );
364        }
365
366        while ( true )
367        {
368            // Break if we get something which is not a '.'
369            if ( !Strings.isCharASCII( str, pos.start, '.' ) )
370            {
371                break;
372            }
373
374            pos.start++;
375
376            if ( !parseNumber( str, pos ) )
377            {
378                throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start );
379            }
380        }
381    }
382
383
384
385
386    /**
387     * Parse an OID.
388     *
389     * numericoid = number 1*( '.' number )
390     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
391     *
392     * @param bytes The OID to parse
393     * @param pos The current position in the string
394     * @throws ParseException If we don't have a valid OID
395     */
396    private static void parseOID( byte[] bytes, Position pos ) throws ParseException
397    {
398        // We have an OID
399        parseNumber( bytes, pos );
400
401        // We must have at least one '.' number
402        if ( !Strings.isCharASCII( bytes, pos.start, '.' ) )
403        {
404            throw new ParseException( I18n.err( I18n.ERR_13221_INVALID_OID_MISSING_DOT ), pos.start );
405        }
406
407        pos.start++;
408
409        if ( !parseNumber( bytes, pos ) )
410        {
411            throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start );
412        }
413
414        while ( true )
415        {
416            // Break if we get something which is not a '.'
417            if ( !Strings.isCharASCII( bytes, pos.start, '.' ) )
418            {
419                break;
420            }
421
422            pos.start++;
423
424            if ( !parseNumber( bytes, pos ) )
425            {
426                throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos.start );
427            }
428        }
429    }
430
431
432    /**
433     * Parse an attribute. The grammar is :
434     * attributedescription = attributetype options
435     * attributetype = oid
436     * oid = descr / numericoid
437     * descr = keystring
438     * numericoid = number 1*( '.' number )
439     * options = *( ';' option )
440     * option = 1*keychar
441     * keystring = leadkeychar *keychar
442     * leadkeychar = 'a'-z' | 'A'-'Z'
443     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
444     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
445     *
446     * @param str The parsed attribute,
447     * @param pos The position of the attribute in the current string
448     * @param withOption A flag set if we want to parse the options
449     * @param relaxed A flag set if we want to parse without being too strict
450     * @return The parsed attribute if valid
451     * @throws ParseException If we had an issue while parsing the attribute
452     */
453    public static String parseAttribute( char[] str, Position pos, boolean withOption, boolean relaxed )
454        throws ParseException
455    {
456        // We must have an OID or an DESCR first
457        char c = Strings.charAt( str, pos.start );
458
459        if ( c == '\0' )
460        {
461            throw new ParseException( I18n.err( I18n.ERR_13222_EMPTY_ATTRIBUTE ), pos.start );
462        }
463
464        int start = pos.start;
465
466        if ( Chars.isAlpha( c ) )
467        {
468            // A DESCR
469            pos.start++;
470
471            while ( Chars.isAlphaDigitMinus( str, pos.start ) || ( relaxed && Chars.isCharASCII( str, pos.start, '_' ) ) )
472            {
473                pos.start++;
474            }
475
476            // Parse the options if needed
477            if ( withOption )
478            {
479                parseOptions( str, pos );
480            }
481
482            return new String( str, start, pos.start - start );
483        }
484        else if ( Chars.isDigit( c ) )
485        {
486            // An OID
487            pos.start++;
488
489            // Parse the OID
490            parseOID( str, pos );
491
492            // Parse the options
493            if ( withOption )
494            {
495                parseOptions( str, pos );
496            }
497
498            return new String( str,  start, pos.start - start );
499        }
500        else
501        {
502            throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos.start );
503        }
504    }
505
506
507
508
509    /**
510     * Parse an attribute. The grammar is :
511     * attributedescription = attributetype options
512     * attributetype = oid
513     * oid = descr / numericoid
514     * descr = keystring
515     * numericoid = number 1*( '.' number )
516     * options = *( ';' option )
517     * option = 1*keychar
518     * keystring = leadkeychar *keychar
519     * leadkeychar = 'a'-z' | 'A'-'Z'
520     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
521     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
522     *
523     * @param bytes The parsed attribute,
524     * @param pos The position of the attribute in the current string
525     * @param withOption A flag set if we want to parse the options
526     * @param relaxed A flag set if we want to parse without being too strict
527     * @return The parsed attribute if valid
528     * @throws ParseException If we had an issue while parsing the attribute
529     */
530    public static String parseAttribute( byte[] bytes, Position pos, boolean withOption, boolean relaxed )
531        throws ParseException
532    {
533        // We must have an OID or an DESCR first
534        byte b = Strings.byteAt( bytes, pos.start );
535
536        if ( b == '\0' )
537        {
538            throw new ParseException( I18n.err( I18n.ERR_13222_EMPTY_ATTRIBUTE ), pos.start );
539        }
540
541        int start = pos.start;
542
543        if ( Chars.isAlpha( b ) )
544        {
545            // A DESCR
546            while ( Chars.isAlphaDigitMinus( bytes, pos.start ) || ( relaxed && Strings.isCharASCII( bytes, pos.start, '_' ) ) )
547            {
548                pos.start++;
549            }
550
551            // Parse the options if needed
552            if ( withOption )
553            {
554                parseOptions( bytes, pos );
555            }
556
557            return Strings.utf8ToString( bytes, start, pos.start - start );
558        }
559        else if ( Chars.isDigit( b ) )
560        {
561            // Parse the OID
562            parseOID( bytes, pos );
563
564            // Parse the options
565            if ( withOption )
566            {
567                parseOptions( bytes, pos );
568            }
569
570            return Strings.utf8ToString( bytes, start, pos.start - start );
571        }
572        else
573        {
574            throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos.start );
575        }
576    }
577
578
579    /**
580     * A method to apply a modification to an existing entry.
581     * 
582     * @param entry The entry on which we want to apply a modification
583     * @param modification the Modification to be applied
584     * @throws LdapException if some operation fails.
585     */
586    public static void applyModification( Entry entry, Modification modification ) throws LdapException
587    {
588        Attribute modAttr = modification.getAttribute();
589        String modificationId = modAttr.getUpId();
590
591        switch ( modification.getOperation() )
592        {
593            case ADD_ATTRIBUTE:
594                Attribute modifiedAttr = entry.get( modificationId );
595
596                if ( modifiedAttr == null )
597                {
598                    // The attribute should be added.
599                    entry.put( modAttr );
600                }
601                else
602                {
603                    // The attribute exists : the values can be different,
604                    // so we will just add the new values to the existing ones.
605                    for ( Value value : modAttr )
606                    {
607                        // If the value already exist, nothing is done.
608                        // Note that the attribute *must* have been
609                        // normalized before.
610                        modifiedAttr.add( value );
611                    }
612                }
613
614                break;
615
616            case REMOVE_ATTRIBUTE:
617                if ( modAttr.get() == null )
618                {
619                    // We have no value in the ModificationItem attribute :
620                    // we have to remove the whole attribute from the initial
621                    // entry
622                    entry.removeAttributes( modificationId );
623                }
624                else
625                {
626                    // We just have to remove the values from the original
627                    // entry, if they exist.
628                    modifiedAttr = entry.get( modificationId );
629
630                    if ( modifiedAttr == null )
631                    {
632                        break;
633                    }
634
635                    for ( Value value : modAttr )
636                    {
637                        // If the value does not exist, nothing is done.
638                        // Note that the attribute *must* have been
639                        // normalized before.
640                        modifiedAttr.remove( value );
641                    }
642
643                    if ( modifiedAttr.size() == 0 )
644                    {
645                        // If this was the last value, remove the attribute
646                        entry.removeAttributes( modifiedAttr.getUpId() );
647                    }
648                }
649
650                break;
651
652            case REPLACE_ATTRIBUTE:
653                if ( modAttr.get() == null )
654                {
655                    // If the modification does not have any value, we have
656                    // to delete the attribute from the entry.
657                    entry.removeAttributes( modificationId );
658                }
659                else
660                {
661                    // otherwise, just substitute the existing attribute.
662                    entry.put( modAttr );
663                }
664
665                break;
666            default:
667                break;
668        }
669    }
670
671
672    /**
673     * Convert a BasicAttributes or a AttributesImpl to an Entry
674     *
675     * @param attributes the BasicAttributes or AttributesImpl instance to convert
676     * @param dn The Dn which is needed by the Entry
677     * @return An instance of a Entry object
678     * 
679     * @throws LdapException If we get an invalid attribute
680     */
681    public static Entry toEntry( Attributes attributes, Dn dn ) throws LdapException
682    {
683        if ( attributes instanceof BasicAttributes )
684        {
685            try
686            {
687                Entry entry = new DefaultEntry( dn );
688
689                for ( NamingEnumeration<? extends javax.naming.directory.Attribute> attrs = attributes.getAll(); attrs
690                    .hasMoreElements(); )
691                {
692                    javax.naming.directory.Attribute attr = attrs.nextElement();
693
694                    Attribute entryAttribute = toApiAttribute( attr );
695
696                    if ( entryAttribute != null )
697                    {
698                        entry.put( entryAttribute );
699                    }
700                }
701
702                return entry;
703            }
704            catch ( LdapException ne )
705            {
706                throw new LdapInvalidAttributeTypeException( ne.getMessage(), ne );
707            }
708        }
709        else
710        {
711            return null;
712        }
713    }
714
715
716    /**
717     * Converts an {@link Entry} to an {@link Attributes}.
718     *
719     * @param entry
720     *      the {@link Entry} to convert
721     * @return
722     *      the equivalent {@link Attributes}
723     */
724    public static Attributes toAttributes( Entry entry )
725    {
726        if ( entry != null )
727        {
728            Attributes attributes = new BasicAttributes( true );
729
730            // Looping on attributes
731            for ( Iterator<Attribute> attributeIterator = entry.iterator(); attributeIterator.hasNext(); )
732            {
733                Attribute entryAttribute = attributeIterator.next();
734
735                attributes.put( toJndiAttribute( entryAttribute ) );
736            }
737
738            return attributes;
739        }
740
741        return null;
742    }
743
744
745    /**
746     * Converts an {@link Attribute} to a JNDI Attribute.
747     *
748     * @param attribute the {@link Attribute} to convert
749     * @return the equivalent JNDI Attribute
750     */
751    public static javax.naming.directory.Attribute toJndiAttribute( Attribute attribute )
752    {
753        if ( attribute != null )
754        {
755            javax.naming.directory.Attribute jndiAttribute = new BasicAttribute( attribute.getUpId() );
756
757            // Looping on values
758            for ( Iterator<Value> valueIterator = attribute.iterator(); valueIterator.hasNext(); )
759            {
760                Value value = valueIterator.next();
761                
762                if ( value.isHumanReadable() )
763                {
764                    jndiAttribute.add( value.getString() );
765                }
766                else
767                {
768                    jndiAttribute.add( value.getBytes() );
769                }
770            }
771
772            return jndiAttribute;
773        }
774
775        return null;
776    }
777
778
779    /**
780     * Convert a JNDI Attribute to an LDAP API Attribute
781     *
782     * @param jndiAttribute the JNDI Attribute instance to convert
783     * @return An instance of a LDAP API Attribute object
784     * @throws LdapInvalidAttributeValueException If the attribute is invalid
785     */
786    public static Attribute toApiAttribute( javax.naming.directory.Attribute jndiAttribute )
787        throws LdapInvalidAttributeValueException
788    {
789        if ( jndiAttribute == null )
790        {
791            return null;
792        }
793
794        try
795        {
796            Attribute attribute = new DefaultAttribute( jndiAttribute.getID() );
797
798            for ( NamingEnumeration<?> values = jndiAttribute.getAll(); values.hasMoreElements(); )
799            {
800                Object value = values.nextElement();
801
802                if ( value instanceof String )
803                {
804                    attribute.add( ( String ) value );
805                }
806                else if ( value instanceof byte[] )
807                {
808                    attribute.add( ( byte[] ) value );
809                }
810                else
811                {
812                    attribute.add( ( String ) null );
813                }
814            }
815
816            return attribute;
817        }
818        catch ( NamingException ne )
819        {
820            return null;
821        }
822    }
823}