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.shared.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.shared.i18n.I18n;
034import org.apache.directory.shared.ldap.model.exception.LdapException;
035import org.apache.directory.shared.ldap.model.exception.LdapInvalidAttributeTypeException;
036import org.apache.directory.shared.ldap.model.exception.LdapInvalidAttributeValueException;
037import org.apache.directory.shared.ldap.model.name.Dn;
038import org.apache.directory.shared.util.Chars;
039import org.apache.directory.shared.util.Position;
040import org.apache.directory.shared.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( String 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( String filter, Position pos )
200    {
201        char c = Strings.charAt(filter, pos.start);
202
203        switch ( c )
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( String 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( String str, Position pos, boolean withOption ) throws ParseException
302    {
303        // We must have an OID or an DESCR first
304        char c = Strings.charAt(str, pos.start);
305
306        if ( c == '\0' )
307        {
308            throw new ParseException( I18n.err( I18n.ERR_04346 ), pos.start );
309        }
310
311        int start = pos.start;
312
313        if ( Chars.isAlpha(c) )
314        {
315            // A DESCR
316            pos.start++;
317
318            while ( Chars.isAlphaDigitMinus(str, pos.start) )
319            {
320                pos.start++;
321            }
322
323            // Parse the options if needed
324            if ( withOption )
325            {
326                parseOptions( str, pos );
327            }
328
329            return str.substring( start, pos.start );
330        }
331        else if ( Chars.isDigit(c) )
332        {
333            // An OID
334            pos.start++;
335
336            // Parse the OID
337            parseOID( str, pos );
338
339            // Parse the options
340            if ( withOption )
341            {
342                parseOptions( str, pos );
343            }
344
345            return str.substring( start, pos.start );
346        }
347        else
348        {
349            throw new ParseException( I18n.err( I18n.ERR_04347 ), pos.start );
350        }
351    }
352
353
354    /**
355     * A method to apply a modification to an existing entry.
356     * 
357     * @param entry The entry on which we want to apply a modification
358     * @param modification the Modification to be applied
359     * @throws org.apache.directory.shared.ldap.model.exception.LdapException if some operation fails.
360     */
361    public static void applyModification( Entry entry, Modification modification ) throws LdapException
362    {
363        Attribute modAttr = modification.getAttribute();
364        String modificationId = modAttr.getId();
365
366        switch ( modification.getOperation() )
367        {
368            case ADD_ATTRIBUTE:
369                Attribute modifiedAttr = entry.get( modificationId );
370
371                if ( modifiedAttr == null )
372                {
373                    // The attribute should be added.
374                    entry.put( modAttr );
375                }
376                else
377                {
378                    // The attribute exists : the values can be different,
379                    // so we will just add the new values to the existing ones.
380                    for ( Value<?> value : modAttr )
381                    {
382                        // If the value already exist, nothing is done.
383                        // Note that the attribute *must* have been
384                        // normalized before.
385                        modifiedAttr.add( value );
386                    }
387                }
388
389                break;
390
391            case REMOVE_ATTRIBUTE:
392                if ( modAttr.get() == null )
393                {
394                    // We have no value in the ModificationItem attribute :
395                    // we have to remove the whole attribute from the initial
396                    // entry
397                    entry.removeAttributes( modificationId );
398                }
399                else
400                {
401                    // We just have to remove the values from the original
402                    // entry, if they exist.
403                    modifiedAttr = entry.get( modificationId );
404
405                    if ( modifiedAttr == null )
406                    {
407                        break;
408                    }
409
410                    for ( Value<?> value : modAttr )
411                    {
412                        // If the value does not exist, nothing is done.
413                        // Note that the attribute *must* have been
414                        // normalized before.
415                        modifiedAttr.remove( value );
416                    }
417
418                    if ( modifiedAttr.size() == 0 )
419                    {
420                        // If this was the last value, remove the attribute
421                        entry.removeAttributes( modifiedAttr.getId() );
422                    }
423                }
424
425                break;
426
427            case REPLACE_ATTRIBUTE:
428                if ( modAttr.get() == null )
429                {
430                    // If the modification does not have any value, we have
431                    // to delete the attribute from the entry.
432                    entry.removeAttributes( modificationId );
433                }
434                else
435                {
436                    // otherwise, just substitute the existing attribute.
437                    entry.put( modAttr );
438                }
439
440                break;
441            default:
442                break;
443        }
444    }
445
446
447    /**
448     * Convert a BasicAttributes or a AttributesImpl to an Entry
449     *
450     * @param attributes the BasicAttributes or AttributesImpl instance to convert
451     * @param dn The Dn which is needed by the Entry
452     * @return An instance of a Entry object
453     * 
454     * @throws LdapException If we get an invalid attribute
455     */
456    public static Entry toEntry( Attributes attributes, Dn dn ) throws LdapException
457    {
458        if ( attributes instanceof BasicAttributes )
459        {
460            try
461            {
462                Entry entry = new DefaultEntry( dn );
463
464                for ( NamingEnumeration<? extends javax.naming.directory.Attribute> attrs = attributes.getAll(); attrs.hasMoreElements(); )
465                {
466                    javax.naming.directory.Attribute attr = attrs.nextElement();
467
468                    Attribute entryAttribute = toApiAttribute( attr );
469
470                    if ( entryAttribute != null )
471                    {
472                        entry.put( entryAttribute );
473                    }
474                }
475
476                return entry;
477            }
478            catch ( LdapException ne )
479            {
480                throw new LdapInvalidAttributeTypeException( ne.getMessage(), ne );
481            }
482        }
483        else
484        {
485            return null;
486        }
487    }
488
489
490    /**
491     * Converts an {@link Entry} to an {@link Attributes}.
492     *
493     * @param entry
494     *      the {@link Entry} to convert
495     * @return
496     *      the equivalent {@link Attributes}
497     */
498    public static Attributes toAttributes( Entry entry )
499    {
500        if ( entry != null )
501        {
502            Attributes attributes = new BasicAttributes( true );
503
504            // Looping on attributes
505            for ( Iterator<Attribute> attributeIterator = entry.iterator(); attributeIterator.hasNext(); )
506            {
507                Attribute entryAttribute = ( Attribute ) attributeIterator.next();
508
509                attributes.put( toJndiAttribute( entryAttribute ) );
510            }
511
512            return attributes;
513        }
514
515        return null;
516    }
517
518
519    /**
520     * Converts an {@link Attribute} to a JNDI Attribute.
521     *
522     * @param attribute the {@link Attribute} to convert
523     * @return the equivalent JNDI Attribute
524     */
525    public static javax.naming.directory.Attribute toJndiAttribute( Attribute attribute )
526    {
527        if ( attribute != null )
528        {
529            javax.naming.directory.Attribute jndiAttribute = new BasicAttribute( attribute.getId() );
530
531            // Looping on values
532            for ( Iterator<Value<?>> valueIterator = attribute.iterator(); valueIterator.hasNext(); )
533            {
534                Value<?> value = valueIterator.next();
535                jndiAttribute.add( value.getValue() );
536            }
537
538            return jndiAttribute;
539        }
540
541        return null;
542    }
543
544
545    /**
546     * Convert a JNDI Attribute to an LDAP API Attribute
547     *
548     * @param jndiAttribute the JNDI Attribute instance to convert
549     * @return An instance of a LDAP API Attribute object
550     */
551    public static Attribute toApiAttribute( javax.naming.directory.Attribute jndiAttribute ) throws LdapInvalidAttributeValueException
552    {
553        if ( jndiAttribute == null )
554        {
555            return null;
556        }
557
558        try
559        {
560            Attribute attribute = new DefaultAttribute( jndiAttribute.getID() );
561
562            for ( NamingEnumeration<?> values = jndiAttribute.getAll(); values.hasMoreElements(); )
563            {
564                Object value = values.nextElement();
565
566                
567                if ( value instanceof String )
568                {
569                    attribute.add( ( String ) value );
570                }
571                else if ( value instanceof byte[] )
572                {
573                    attribute.add( ( byte[] ) value );
574                }
575                else
576                {
577                    attribute.add( ( String ) null );
578                }
579            }
580
581            return attribute;
582        }
583        catch ( NamingException ne )
584        {
585            return null;
586        }
587    }
588}