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 converted 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     * options = *( ';' option )
170     * option = 1*keychar
171     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
172     */
173    private static void parseOptions( byte[] str, Position pos ) throws ParseException
174    {
175        while ( Strings.isCharASCII( str, pos.start, ';' ) )
176        {
177            pos.start++;
178
179            // We have an option
180            if ( !Chars.isAlphaDigitMinus( str, pos.start ) )
181            {
182                // We must have at least one keychar
183                throw new ParseException( I18n.err( I18n.ERR_04343 ), pos.start );
184            }
185
186            pos.start++;
187
188            while ( Chars.isAlphaDigitMinus( str, pos.start ) )
189            {
190                pos.start++;
191            }
192        }
193    }
194
195
196    /**
197     * Parse a number :
198     * 
199     * number = '0' | '1'..'9' digits
200     * digits = '0'..'9'*
201     * 
202     * @return true if a number has been found
203     */
204    private static boolean parseNumber( byte[] filter, Position pos )
205    {
206        byte b = Strings.byteAt( filter, pos.start );
207
208        switch ( b )
209        {
210            case '0':
211                // If we get a starting '0', we should get out
212                pos.start++;
213                return true;
214
215            case '1':
216            case '2':
217            case '3':
218            case '4':
219            case '5':
220            case '6':
221            case '7':
222            case '8':
223            case '9':
224                pos.start++;
225                break;
226
227            default:
228                // Not a number.
229                return false;
230        }
231
232        while ( Chars.isDigit( filter, pos.start ) )
233        {
234            pos.start++;
235        }
236
237        return true;
238    }
239
240
241    /**
242     * 
243     * Parse an OID.
244     *
245     * numericoid = number 1*( '.' number )
246     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
247     *
248     * @param str The OID to parse
249     * @param pos The current position in the string
250     * @throws ParseException If we don't have a valid OID
251     */
252    private static void parseOID( byte[] str, Position pos ) throws ParseException
253    {
254        // We have an OID
255        parseNumber( str, pos );
256
257        // We must have at least one '.' number
258        if ( !Strings.isCharASCII( str, pos.start, '.' ) )
259        {
260            throw new ParseException( I18n.err( I18n.ERR_04344 ), pos.start );
261        }
262
263        pos.start++;
264
265        if ( !parseNumber( str, pos ) )
266        {
267            throw new ParseException( I18n.err( I18n.ERR_04345 ), pos.start );
268        }
269
270        while ( true )
271        {
272            // Break if we get something which is not a '.'
273            if ( !Strings.isCharASCII( str, pos.start, '.' ) )
274            {
275                break;
276            }
277
278            pos.start++;
279
280            if ( !parseNumber( str, pos ) )
281            {
282                throw new ParseException( I18n.err( I18n.ERR_04345 ), pos.start );
283            }
284        }
285    }
286
287
288    /**
289     * Parse an attribute. The grammar is :
290     * attributedescription = attributetype options
291     * attributetype = oid
292     * oid = descr / numericoid
293     * descr = keystring
294     * numericoid = number 1*( '.' number )
295     * options = *( ';' option )
296     * option = 1*keychar
297     * keystring = leadkeychar *keychar
298     * leadkeychar = 'a'-z' | 'A'-'Z'
299     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
300     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
301     *
302     * @param str The parsed attribute,
303     * @param pos The position of the attribute in the current string
304     * @param withOption A flag telling of the attribute has an option
305     * @param relaxed A flag used to tell the parser to be in relaxed mode
306     * @return The parsed attribute if valid
307     * @throws ParseException If we faced an error while parsing the value
308     */
309    public static String parseAttribute( byte[] str, Position pos, boolean withOption, boolean relaxed )
310        throws ParseException
311    {
312        // We must have an OID or an DESCR first
313        byte b = Strings.byteAt( str, pos.start );
314
315        if ( b == '\0' )
316        {
317            throw new ParseException( I18n.err( I18n.ERR_04346 ), pos.start );
318        }
319
320        int start = pos.start;
321
322        if ( Chars.isAlpha( b ) )
323        {
324            // A DESCR
325            pos.start++;
326
327            while ( Chars.isAlphaDigitMinus( str, pos.start ) || ( relaxed && Chars.isUnderscore( str, pos.start ) ) )
328            {
329                pos.start++;
330            }
331
332            // Parse the options if needed
333            if ( withOption )
334            {
335                parseOptions( str, pos );
336            }
337
338            return Strings.getString( str, start, pos.start - start, "UTF-8" );
339        }
340        else if ( Chars.isDigit( b ) )
341        {
342            // An OID
343            pos.start++;
344
345            // Parse the OID
346            parseOID( str, pos );
347
348            // Parse the options
349            if ( withOption )
350            {
351                parseOptions( str, pos );
352            }
353
354            return Strings.getString( str,  start, pos.start - start, "UTF-8" );
355        }
356        else
357        {
358            throw new ParseException( I18n.err( I18n.ERR_04347 ), pos.start );
359        }
360    }
361
362
363    /**
364     * A method to apply a modification to an existing entry.
365     * 
366     * @param entry The entry on which we want to apply a modification
367     * @param modification the Modification to be applied
368     * @throws org.apache.directory.api.ldap.model.exception.LdapException if some operation fails.
369     */
370    public static void applyModification( Entry entry, Modification modification ) throws LdapException
371    {
372        Attribute modAttr = modification.getAttribute();
373        String modificationId = modAttr.getUpId();
374
375        switch ( modification.getOperation() )
376        {
377            case ADD_ATTRIBUTE:
378                Attribute modifiedAttr = entry.get( modificationId );
379
380                if ( modifiedAttr == null )
381                {
382                    // The attribute should be added.
383                    entry.put( modAttr );
384                }
385                else
386                {
387                    // The attribute exists : the values can be different,
388                    // so we will just add the new values to the existing ones.
389                    for ( Value<?> value : modAttr )
390                    {
391                        // If the value already exist, nothing is done.
392                        // Note that the attribute *must* have been
393                        // normalized before.
394                        modifiedAttr.add( value );
395                    }
396                }
397
398                break;
399
400            case REMOVE_ATTRIBUTE:
401                if ( modAttr.get() == null )
402                {
403                    // We have no value in the ModificationItem attribute :
404                    // we have to remove the whole attribute from the initial
405                    // entry
406                    entry.removeAttributes( modificationId );
407                }
408                else
409                {
410                    // We just have to remove the values from the original
411                    // entry, if they exist.
412                    modifiedAttr = entry.get( modificationId );
413
414                    if ( modifiedAttr == null )
415                    {
416                        break;
417                    }
418
419                    for ( Value<?> value : modAttr )
420                    {
421                        // If the value does not exist, nothing is done.
422                        // Note that the attribute *must* have been
423                        // normalized before.
424                        modifiedAttr.remove( value );
425                    }
426
427                    if ( modifiedAttr.size() == 0 )
428                    {
429                        // If this was the last value, remove the attribute
430                        entry.removeAttributes( modifiedAttr.getUpId() );
431                    }
432                }
433
434                break;
435
436            case REPLACE_ATTRIBUTE:
437                if ( modAttr.get() == null )
438                {
439                    // If the modification does not have any value, we have
440                    // to delete the attribute from the entry.
441                    entry.removeAttributes( modificationId );
442                }
443                else
444                {
445                    // otherwise, just substitute the existing attribute.
446                    entry.put( modAttr );
447                }
448
449                break;
450            default:
451                break;
452        }
453    }
454
455
456    /**
457     * Convert a BasicAttributes or a AttributesImpl to an Entry
458     *
459     * @param attributes the BasicAttributes or AttributesImpl instance to convert
460     * @param dn The Dn which is needed by the Entry
461     * @return An instance of a Entry object
462     * 
463     * @throws LdapException If we get an invalid attribute
464     */
465    public static Entry toEntry( Attributes attributes, Dn dn ) throws LdapException
466    {
467        if ( attributes instanceof BasicAttributes )
468        {
469            try
470            {
471                Entry entry = new DefaultEntry( dn );
472
473                for ( NamingEnumeration<? extends javax.naming.directory.Attribute> attrs = attributes.getAll(); attrs
474                    .hasMoreElements(); )
475                {
476                    javax.naming.directory.Attribute attr = attrs.nextElement();
477
478                    Attribute entryAttribute = toApiAttribute( attr );
479
480                    if ( entryAttribute != null )
481                    {
482                        entry.put( entryAttribute );
483                    }
484                }
485
486                return entry;
487            }
488            catch ( LdapException ne )
489            {
490                throw new LdapInvalidAttributeTypeException( ne.getMessage(), ne );
491            }
492        }
493        else
494        {
495            return null;
496        }
497    }
498
499
500    /**
501     * Converts an {@link Entry} to an {@link Attributes}.
502     *
503     * @param entry
504     *      the {@link Entry} to convert
505     * @return
506     *      the equivalent {@link Attributes}
507     */
508    public static Attributes toAttributes( Entry entry )
509    {
510        if ( entry != null )
511        {
512            Attributes attributes = new BasicAttributes( true );
513
514            // Looping on attributes
515            for ( Iterator<Attribute> attributeIterator = entry.iterator(); attributeIterator.hasNext(); )
516            {
517                Attribute entryAttribute = attributeIterator.next();
518
519                attributes.put( toJndiAttribute( entryAttribute ) );
520            }
521
522            return attributes;
523        }
524
525        return null;
526    }
527
528
529    /**
530     * Converts an {@link Attribute} to a JNDI Attribute.
531     *
532     * @param attribute the {@link Attribute} to convert
533     * @return the equivalent JNDI Attribute
534     */
535    public static javax.naming.directory.Attribute toJndiAttribute( Attribute attribute )
536    {
537        if ( attribute != null )
538        {
539            javax.naming.directory.Attribute jndiAttribute = new BasicAttribute( attribute.getUpId() );
540
541            // Looping on values
542            for ( Iterator<Value<?>> valueIterator = attribute.iterator(); valueIterator.hasNext(); )
543            {
544                Value<?> value = valueIterator.next();
545                jndiAttribute.add( value.getValue() );
546            }
547
548            return jndiAttribute;
549        }
550
551        return null;
552    }
553
554
555    /**
556     * Convert a JNDI Attribute to an LDAP API Attribute
557     *
558     * @param jndiAttribute the JNDI Attribute instance to convert
559     * @return An instance of a LDAP API Attribute object
560     * @throws LdapInvalidAttributeValueException If we can't convert some value
561     */
562    public static Attribute toApiAttribute( javax.naming.directory.Attribute jndiAttribute )
563        throws LdapInvalidAttributeValueException
564    {
565        if ( jndiAttribute == null )
566        {
567            return null;
568        }
569
570        try
571        {
572            Attribute attribute = new DefaultAttribute( jndiAttribute.getID() );
573
574            for ( NamingEnumeration<?> values = jndiAttribute.getAll(); values.hasMoreElements(); )
575            {
576                Object value = values.nextElement();
577
578                if ( value instanceof String )
579                {
580                    attribute.add( ( String ) value );
581                }
582                else if ( value instanceof byte[] )
583                {
584                    attribute.add( ( byte[] ) value );
585                }
586                else
587                {
588                    attribute.add( ( String ) null );
589                }
590            }
591
592            return attribute;
593        }
594        catch ( NamingException ne )
595        {
596            return null;
597        }
598    }
599}