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   *    https://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 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 }