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
051    /**
052     * Check if an attribute contains a value. The test is case insensitive,
053     * and the value is supposed to be a String. If the value is a byte[],
054     * then the case sensitivity is useless.
055     *
056     * @param attr The attribute to check
057     * @param value The value to look for
058     * @return true if the value is present in the attribute
059     */
060    public static boolean containsValueCaseIgnore( javax.naming.directory.Attribute attr, Object value )
061    {
062        // quick bypass test
063        if ( attr.contains( value ) )
064        {
065            return true;
066        }
067
068        try
069        {
070            if ( value instanceof String )
071            {
072                String strVal = ( String ) value;
073
074                NamingEnumeration<?> attrVals = attr.getAll();
075
076                while ( attrVals.hasMoreElements() )
077                {
078                    Object attrVal = attrVals.nextElement();
079
080                    if ( attrVal instanceof String && strVal.equalsIgnoreCase( ( String ) attrVal ) )
081                    {
082                        return true;
083                    }
084                }
085            }
086            else
087            {
088                byte[] valueBytes = ( byte[] ) value;
089
090                NamingEnumeration<?> attrVals = attr.getAll();
091
092                while ( attrVals.hasMoreElements() )
093                {
094                    Object attrVal = attrVals.nextElement();
095
096                    if ( attrVal instanceof byte[] && Arrays.equals( ( byte[] ) attrVal, valueBytes ) )
097                    {
098                        return true;
099                    }
100                }
101            }
102        }
103        catch ( NamingException ne )
104        {
105            return false;
106        }
107
108        return false;
109    }
110
111
112    /**
113     * Check if the attributes is a BasicAttributes, and if so, switch
114     * the case sensitivity to false to avoid tricky problems in the server.
115     * (Ldap attributeTypes are *always* case insensitive)
116     * 
117     * @param attributes The Attributes to check
118     */
119    public static Attributes toCaseInsensitive( Attributes attributes )
120    {
121        if ( attributes == null )
122        {
123            return attributes;
124        }
125
126        if ( attributes instanceof BasicAttributes )
127        {
128            if ( attributes.isCaseIgnored() )
129            {
130                // Just do nothing if the Attributes is already case insensitive
131                return attributes;
132            }
133            else
134            {
135                // Ok, bad news : we have to create a new BasicAttributes
136                // which will be case insensitive
137                Attributes newAttrs = new BasicAttributes( true );
138
139                NamingEnumeration<?> attrs = attributes.getAll();
140
141                if ( attrs != null )
142                {
143                    // Iterate through the attributes now
144                    while ( attrs.hasMoreElements() )
145                    {
146                        newAttrs.put( ( javax.naming.directory.Attribute ) attrs.nextElement() );
147                    }
148                }
149
150                return newAttrs;
151            }
152        }
153        else
154        {
155            // we can safely return the attributes if it's not a BasicAttributes
156            return attributes;
157        }
158    }
159
160
161    /**
162     * Parse attribute's options :
163     * 
164     * options = *( ';' option )
165     * option = 1*keychar
166     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
167     */
168    private static void parseOptions( byte[] str, Position pos ) throws ParseException
169    {
170        while ( Strings.isCharASCII( str, pos.start, ';' ) )
171        {
172            pos.start++;
173
174            // We have an option
175            if ( !Chars.isAlphaDigitMinus( str, pos.start ) )
176            {
177                // We must have at least one keychar
178                throw new ParseException( I18n.err( I18n.ERR_04343 ), pos.start );
179            }
180
181            pos.start++;
182
183            while ( Chars.isAlphaDigitMinus( str, pos.start ) )
184            {
185                pos.start++;
186            }
187        }
188    }
189
190
191    /**
192     * Parse a number :
193     * 
194     * number = '0' | '1'..'9' digits
195     * digits = '0'..'9'*
196     * 
197     * @return true if a number has been found
198     */
199    private static boolean parseNumber( byte[] filter, Position pos )
200    {
201        byte b = Strings.byteAt( filter, pos.start );
202
203        switch ( b )
204        {
205            case '0':
206                // If we get a starting '0', we should get out
207                pos.start++;
208                return true;
209
210            case '1':
211            case '2':
212            case '3':
213            case '4':
214            case '5':
215            case '6':
216            case '7':
217            case '8':
218            case '9':
219                pos.start++;
220                break;
221
222            default:
223                // Not a number.
224                return false;
225        }
226
227        while ( Chars.isDigit( filter, pos.start ) )
228        {
229            pos.start++;
230        }
231
232        return true;
233    }
234
235
236    /**
237     * 
238     * Parse an OID.
239     *
240     * numericoid = number 1*( '.' number )
241     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
242     *
243     * @param str The OID to parse
244     * @param pos The current position in the string
245     * @throws ParseException If we don't have a valid OID
246     */
247    private static void parseOID( byte[] str, Position pos ) throws ParseException
248    {
249        // We have an OID
250        parseNumber( str, pos );
251
252        // We must have at least one '.' number
253        if ( !Strings.isCharASCII( str, pos.start, '.' ) )
254        {
255            throw new ParseException( I18n.err( I18n.ERR_04344 ), pos.start );
256        }
257
258        pos.start++;
259
260        if ( !parseNumber( str, pos ) )
261        {
262            throw new ParseException( I18n.err( I18n.ERR_04345 ), pos.start );
263        }
264
265        while ( true )
266        {
267            // Break if we get something which is not a '.'
268            if ( !Strings.isCharASCII( str, pos.start, '.' ) )
269            {
270                break;
271            }
272
273            pos.start++;
274
275            if ( !parseNumber( str, pos ) )
276            {
277                throw new ParseException( I18n.err( I18n.ERR_04345 ), pos.start );
278            }
279        }
280    }
281
282
283    /**
284     * Parse an attribute. The grammar is :
285     * attributedescription = attributetype options
286     * attributetype = oid
287     * oid = descr / numericoid
288     * descr = keystring
289     * numericoid = number 1*( '.' number )
290     * options = *( ';' option )
291     * option = 1*keychar
292     * keystring = leadkeychar *keychar
293     * leadkeychar = 'a'-z' | 'A'-'Z'
294     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
295     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
296     *
297     * @param str The parsed attribute,
298     * @param pos The position of the attribute in the current string
299     * @return The parsed attribute if valid
300     */
301    public static String parseAttribute( byte[] str, Position pos, boolean withOption, boolean relaxed )
302        throws ParseException
303    {
304        // We must have an OID or an DESCR first
305        byte b = Strings.byteAt( str, pos.start );
306
307        if ( b == '\0' )
308        {
309            throw new ParseException( I18n.err( I18n.ERR_04346 ), pos.start );
310        }
311
312        int start = pos.start;
313
314        if ( Chars.isAlpha( b ) )
315        {
316            // A DESCR
317            pos.start++;
318
319            while ( Chars.isAlphaDigitMinus( str, pos.start ) || ( relaxed && Chars.isUnderscore( str, pos.start ) ) )
320            {
321                pos.start++;
322            }
323
324            // Parse the options if needed
325            if ( withOption )
326            {
327                parseOptions( str, pos );
328            }
329
330            return Strings.getString( str, start, pos.start - start, "UTF-8" );
331        }
332        else if ( Chars.isDigit( b ) )
333        {
334            // An OID
335            pos.start++;
336
337            // Parse the OID
338            parseOID( str, pos );
339
340            // Parse the options
341            if ( withOption )
342            {
343                parseOptions( str, pos );
344            }
345
346            return Strings.getString( str,  start, pos.start - start, "UTF-8" );
347        }
348        else
349        {
350            throw new ParseException( I18n.err( I18n.ERR_04347 ), pos.start );
351        }
352    }
353
354
355    /**
356     * A method to apply a modification to an existing entry.
357     * 
358     * @param entry The entry on which we want to apply a modification
359     * @param modification the Modification to be applied
360     * @throws org.apache.directory.api.ldap.model.exception.LdapException if some operation fails.
361     */
362    public static void applyModification( Entry entry, Modification modification ) throws LdapException
363    {
364        Attribute modAttr = modification.getAttribute();
365        String modificationId = modAttr.getUpId();
366
367        switch ( modification.getOperation() )
368        {
369            case ADD_ATTRIBUTE:
370                Attribute modifiedAttr = entry.get( modificationId );
371
372                if ( modifiedAttr == null )
373                {
374                    // The attribute should be added.
375                    entry.put( modAttr );
376                }
377                else
378                {
379                    // The attribute exists : the values can be different,
380                    // so we will just add the new values to the existing ones.
381                    for ( Value<?> value : modAttr )
382                    {
383                        // If the value already exist, nothing is done.
384                        // Note that the attribute *must* have been
385                        // normalized before.
386                        modifiedAttr.add( value );
387                    }
388                }
389
390                break;
391
392            case REMOVE_ATTRIBUTE:
393                if ( modAttr.get() == null )
394                {
395                    // We have no value in the ModificationItem attribute :
396                    // we have to remove the whole attribute from the initial
397                    // entry
398                    entry.removeAttributes( modificationId );
399                }
400                else
401                {
402                    // We just have to remove the values from the original
403                    // entry, if they exist.
404                    modifiedAttr = entry.get( modificationId );
405
406                    if ( modifiedAttr == null )
407                    {
408                        break;
409                    }
410
411                    for ( Value<?> value : modAttr )
412                    {
413                        // If the value does not exist, nothing is done.
414                        // Note that the attribute *must* have been
415                        // normalized before.
416                        modifiedAttr.remove( value );
417                    }
418
419                    if ( modifiedAttr.size() == 0 )
420                    {
421                        // If this was the last value, remove the attribute
422                        entry.removeAttributes( modifiedAttr.getUpId() );
423                    }
424                }
425
426                break;
427
428            case REPLACE_ATTRIBUTE:
429                if ( modAttr.get() == null )
430                {
431                    // If the modification does not have any value, we have
432                    // to delete the attribute from the entry.
433                    entry.removeAttributes( modificationId );
434                }
435                else
436                {
437                    // otherwise, just substitute the existing attribute.
438                    entry.put( modAttr );
439                }
440
441                break;
442            default:
443                break;
444        }
445    }
446
447
448    /**
449     * Convert a BasicAttributes or a AttributesImpl to an Entry
450     *
451     * @param attributes the BasicAttributes or AttributesImpl instance to convert
452     * @param dn The Dn which is needed by the Entry
453     * @return An instance of a Entry object
454     * 
455     * @throws LdapException If we get an invalid attribute
456     */
457    public static Entry toEntry( Attributes attributes, Dn dn ) throws LdapException
458    {
459        if ( attributes instanceof BasicAttributes )
460        {
461            try
462            {
463                Entry entry = new DefaultEntry( dn );
464
465                for ( NamingEnumeration<? extends javax.naming.directory.Attribute> attrs = attributes.getAll(); attrs
466                    .hasMoreElements(); )
467                {
468                    javax.naming.directory.Attribute attr = attrs.nextElement();
469
470                    Attribute entryAttribute = toApiAttribute( attr );
471
472                    if ( entryAttribute != null )
473                    {
474                        entry.put( entryAttribute );
475                    }
476                }
477
478                return entry;
479            }
480            catch ( LdapException ne )
481            {
482                throw new LdapInvalidAttributeTypeException( ne.getMessage(), ne );
483            }
484        }
485        else
486        {
487            return null;
488        }
489    }
490
491
492    /**
493     * Converts an {@link Entry} to an {@link Attributes}.
494     *
495     * @param entry
496     *      the {@link Entry} to convert
497     * @return
498     *      the equivalent {@link Attributes}
499     */
500    public static Attributes toAttributes( Entry entry )
501    {
502        if ( entry != null )
503        {
504            Attributes attributes = new BasicAttributes( true );
505
506            // Looping on attributes
507            for ( Iterator<Attribute> attributeIterator = entry.iterator(); attributeIterator.hasNext(); )
508            {
509                Attribute entryAttribute = attributeIterator.next();
510
511                attributes.put( toJndiAttribute( entryAttribute ) );
512            }
513
514            return attributes;
515        }
516
517        return null;
518    }
519
520
521    /**
522     * Converts an {@link Attribute} to a JNDI Attribute.
523     *
524     * @param attribute the {@link Attribute} to convert
525     * @return the equivalent JNDI Attribute
526     */
527    public static javax.naming.directory.Attribute toJndiAttribute( Attribute attribute )
528    {
529        if ( attribute != null )
530        {
531            javax.naming.directory.Attribute jndiAttribute = new BasicAttribute( attribute.getUpId() );
532
533            // Looping on values
534            for ( Iterator<Value<?>> valueIterator = attribute.iterator(); valueIterator.hasNext(); )
535            {
536                Value<?> value = valueIterator.next();
537                jndiAttribute.add( value.getValue() );
538            }
539
540            return jndiAttribute;
541        }
542
543        return null;
544    }
545
546
547    /**
548     * Convert a JNDI Attribute to an LDAP API Attribute
549     *
550     * @param jndiAttribute the JNDI Attribute instance to convert
551     * @return An instance of a LDAP API Attribute object
552     */
553    public static Attribute toApiAttribute( javax.naming.directory.Attribute jndiAttribute )
554        throws LdapInvalidAttributeValueException
555    {
556        if ( jndiAttribute == null )
557        {
558            return null;
559        }
560
561        try
562        {
563            Attribute attribute = new DefaultAttribute( jndiAttribute.getID() );
564
565            for ( NamingEnumeration<?> values = jndiAttribute.getAll(); values.hasMoreElements(); )
566            {
567                Object value = values.nextElement();
568
569                if ( value instanceof String )
570                {
571                    attribute.add( ( String ) value );
572                }
573                else if ( value instanceof byte[] )
574                {
575                    attribute.add( ( byte[] ) value );
576                }
577                else
578                {
579                    attribute.add( ( String ) null );
580                }
581            }
582
583            return attribute;
584        }
585        catch ( NamingException ne )
586        {
587            return null;
588        }
589    }
590}