View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    * 
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   * 
19   */
20  package org.apache.directory.api.ldap.model.entry;
21  
22  
23  import java.text.ParseException;
24  import java.util.Arrays;
25  import java.util.Iterator;
26  
27  import javax.naming.NamingEnumeration;
28  import javax.naming.NamingException;
29  import javax.naming.directory.Attributes;
30  import javax.naming.directory.BasicAttribute;
31  import javax.naming.directory.BasicAttributes;
32  
33  import org.apache.directory.api.i18n.I18n;
34  import org.apache.directory.api.ldap.model.exception.LdapException;
35  import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeTypeException;
36  import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
37  import org.apache.directory.api.ldap.model.name.Dn;
38  import org.apache.directory.api.util.Chars;
39  import org.apache.directory.api.util.Position;
40  import org.apache.directory.api.util.Strings;
41  
42  
43  /**
44   * A set of utility fuctions for working with Attributes.
45   * 
46   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
47   */
48  public final class AttributeUtils
49  {
50      private AttributeUtils()
51      {
52      }
53  
54  
55      /**
56       * Check if an attribute contains a value. The test is case insensitive,
57       * and the value is supposed to be a String. If the value is a byte[],
58       * then the case sensitivity is useless.
59       *
60       * @param attr The attribute to check
61       * @param value The value to look for
62       * @return true if the value is present in the attribute
63       */
64      public static boolean containsValueCaseIgnore( javax.naming.directory.Attribute attr, Object value )
65      {
66          // quick bypass test
67          if ( attr.contains( value ) )
68          {
69              return true;
70          }
71  
72          try
73          {
74              if ( value instanceof String )
75              {
76                  String strVal = ( String ) value;
77  
78                  NamingEnumeration<?> attrVals = attr.getAll();
79  
80                  while ( attrVals.hasMoreElements() )
81                  {
82                      Object attrVal = attrVals.nextElement();
83  
84                      if ( attrVal instanceof String && strVal.equalsIgnoreCase( ( String ) attrVal ) )
85                      {
86                          return true;
87                      }
88                  }
89              }
90              else
91              {
92                  byte[] valueBytes = ( byte[] ) value;
93  
94                  NamingEnumeration<?> attrVals = attr.getAll();
95  
96                  while ( attrVals.hasMoreElements() )
97                  {
98                      Object attrVal = attrVals.nextElement();
99  
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 }