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.schema;
21  
22  
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.UUID;
28  
29  import org.apache.directory.api.i18n.I18n;
30  import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
31  import org.apache.directory.api.ldap.model.constants.SchemaConstants;
32  import org.apache.directory.api.ldap.model.entry.Attribute;
33  import org.apache.directory.api.ldap.model.entry.Entry;
34  import org.apache.directory.api.ldap.model.entry.Modification;
35  import org.apache.directory.api.ldap.model.entry.Value;
36  import org.apache.directory.api.ldap.model.exception.LdapException;
37  import org.apache.directory.api.util.Strings;
38  
39  
40  /**
41   * Various utility methods for schema functions and objects.
42   * 
43   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
44   */
45  public final class SchemaUtils
46  {
47      /**
48       * Private constructor.
49       */
50      private SchemaUtils()
51      {
52      }
53  
54  
55      /**
56       * Gets the target entry as it would look after a modification operation
57       * were performed on it.
58       * 
59       * @param mods the modifications performed on the entry
60       * @param entry the source entry that is modified
61       * @return the resultant entry after the modifications have taken place
62       * @throws LdapException if there are problems accessing attributes
63       */
64      public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry )
65          throws LdapException
66      {
67          Entry targetEntry = entry.clone();
68  
69          for ( Modification mod : mods )
70          {
71              String id = mod.getAttribute().getId();
72  
73              switch ( mod.getOperation() )
74              {
75                  case REPLACE_ATTRIBUTE:
76                      targetEntry.put( mod.getAttribute() );
77                      break;
78  
79                  case ADD_ATTRIBUTE:
80                      Attribute combined = mod.getAttribute().clone();
81                      Attribute toBeAdded = mod.getAttribute();
82                      Attribute existing = entry.get( id );
83  
84                      if ( existing != null )
85                      {
86                          for ( Value value : existing )
87                          {
88                              combined.add( value );
89                          }
90                      }
91  
92                      for ( Value value : toBeAdded )
93                      {
94                          combined.add( value );
95                      }
96  
97                      targetEntry.put( combined );
98                      break;
99  
100                 case REMOVE_ATTRIBUTE:
101                     Attribute toBeRemoved = mod.getAttribute();
102 
103                     if ( toBeRemoved.size() == 0 )
104                     {
105                         targetEntry.removeAttributes( id );
106                     }
107                     else
108                     {
109                         existing = targetEntry.get( id );
110 
111                         if ( existing != null )
112                         {
113                             for ( Value value : toBeRemoved )
114                             {
115                                 existing.remove( value );
116                             }
117                         }
118                     }
119 
120                     break;
121                     
122                 case INCREMENT_ATTRIBUTE:
123                     // The incremented attribute might not exist
124                     AttributeType attributeType = mod.getAttribute().getAttributeType();
125                     String incrementStr = mod.getAttribute().getString();
126                     int increment = 1;
127                     
128                     if ( !Strings.isEmpty( incrementStr ) )
129                     {
130                         try
131                         { 
132                             increment = Integer.parseInt( incrementStr );
133                         }
134                         catch ( NumberFormatException nfe )
135                         {
136                             throw new IllegalArgumentException( I18n.err( I18n.ERR_13866_MOD_INCREMENT_INVALID_VALUE,  
137                                 attributeType.getName(), incrementStr ) );
138                         }
139                     }
140                     Attribute modified = targetEntry.get( attributeType );
141 
142                     if ( !targetEntry.containsAttribute( attributeType ) )
143                     {
144                         throw new IllegalArgumentException( I18n.err( I18n.ERR_13867_MOD_INCREMENT_NO_ATTRIBUTE,  
145                             attributeType.getName() ) );
146                     }
147 
148                     if ( !SchemaConstants.INTEGER_SYNTAX.equals( modified.getAttributeType().getSyntax().getOid() ) )
149                     {
150                         throw new IllegalArgumentException( I18n.err( I18n.ERR_13868_MOD_INCREMENT_NO_INT_ATTRIBUTE,  
151                             attributeType.getName() ) );
152                     }
153                     else
154                     {
155                         Value[] newValues = new Value[ modified.size() ];
156                         int i = 0;
157                         
158                         for ( Value value : modified )
159                         {
160                             int intValue = Integer.parseInt( value.getNormalized() );
161                             
162                             if ( intValue == Integer.MAX_VALUE )
163                             {
164                                 throw new IllegalArgumentException( I18n.err( I18n.ERR_13869_MOD_INCREMENT_OVERFLOW,  
165                                     attributeType.getName(), intValue ) );
166                             }
167                             
168                             newValues[i++] = new Value( Integer.toString( intValue + increment ) );
169                             modified.remove( value );
170                         }
171                         
172                         modified.add( newValues );
173                     }
174                     
175                     break;
176 
177                 default:
178                     throw new IllegalStateException( I18n.err( I18n.ERR_13775_UNDEFINED_MODIFICATION_TYPE, mod.getOperation() ) );
179             }
180         }
181 
182         return targetEntry;
183     }
184 
185 
186     // ------------------------------------------------------------------------
187     // qdescrs rendering operations
188     // ------------------------------------------------------------------------
189 
190     /**
191      * Renders qdescrs into an existing buffer.
192      * 
193      * @param buf the string buffer to render the quoted description strs into
194      * @param qdescrs the quoted description strings to render
195      * @return the same string buffer that was given for call chaining
196      */
197     public static StringBuilder render( StringBuilder buf, List<String> qdescrs )
198     {
199         if ( ( qdescrs == null ) || qdescrs.isEmpty() )
200         {
201             return buf;
202         }
203         else if ( qdescrs.size() == 1 )
204         {
205             buf.append( "'" ).append( qdescrs.get( 0 ) ).append( "'" );
206         }
207         else
208         {
209             buf.append( "( " );
210 
211             for ( String qdescr : qdescrs )
212             {
213                 buf.append( "'" ).append( qdescr ).append( "' " );
214             }
215 
216             buf.append( ")" );
217         }
218 
219         return buf;
220     }
221 
222 
223     /**
224      * Renders qdescrs into a new buffer.<br>
225      * <pre>
226      * descrs ::= qdescr | '(' WSP qdescrlist WSP ')'
227      * qdescrlist ::= [ qdescr ( SP qdescr )* ]
228      * qdescr     ::= SQUOTE descr SQUOTE
229      * </pre>
230      * 
231      * @param buf the string buffer to render the quoted description strings into
232      * @param qdescrs the quoted description strings to render
233      * @return the string buffer the qdescrs are rendered into
234      */
235     /* No qualifier */static StringBuilder renderQDescrs( StringBuilder buf, List<String> qdescrs )
236     {
237         if ( ( qdescrs == null ) || qdescrs.isEmpty() )
238         {
239             return buf;
240         }
241 
242         if ( qdescrs.size() == 1 )
243         {
244             buf.append( '\'' ).append( qdescrs.get( 0 ) ).append( '\'' );
245         }
246         else
247         {
248             buf.append( "( " );
249 
250             for ( String qdescr : qdescrs )
251             {
252                 buf.append( '\'' ).append( qdescr ).append( "' " );
253             }
254 
255             buf.append( ")" );
256         }
257 
258         return buf;
259     }
260 
261 
262     /**
263      * Renders QDString into a new buffer.<br>
264      * 
265      * @param buf the string buffer to render the quoted description string into
266      * @param qdString the quoted description strings to render
267      * @return the string buffer the qdescrs are rendered into
268      */
269     private static StringBuilder renderQDString( StringBuilder buf, String qdString )
270     {
271         buf.append( '\'' );
272 
273         for ( char c : qdString.toCharArray() )
274         {
275             switch ( c )
276             {
277                 case 0x27:
278                     buf.append( "\\27" );
279                     break;
280 
281                 case 0x5C:
282                     buf.append( "\\5C" );
283                     break;
284 
285                 default:
286                     buf.append( c );
287                     break;
288             }
289         }
290 
291         buf.append( '\'' );
292 
293         return buf;
294     }
295 
296 
297     // ------------------------------------------------------------------------
298     // objectClass list rendering operations
299     // ------------------------------------------------------------------------
300 
301     /**
302      * Renders a list of object classes for things like a list of superior
303      * objectClasses using the ( oid $ oid ) format.
304      * 
305      * @param ocs
306      *            the objectClasses to list
307      * @return a buffer which contains the rendered list
308      */
309     public static StringBuilder render( ObjectClass[] ocs )
310     {
311         StringBuilder buf = new StringBuilder();
312 
313         return render( buf, ocs );
314     }
315 
316 
317     /**
318      * Renders a list of object classes for things like a list of superior
319      * objectClasses using the ( oid $ oid ) format into an existing buffer.
320      * 
321      * @param buf
322      *            the string buffer to render the list of objectClasses into
323      * @param ocs
324      *            the objectClasses to list
325      * @return a buffer which contains the rendered list
326      */
327     public static StringBuilder render( StringBuilder buf, ObjectClass[] ocs )
328     {
329         if ( ocs == null || ocs.length == 0 )
330         {
331             return buf;
332         }
333         else if ( ocs.length == 1 )
334         {
335             buf.append( ocs[0].getName() );
336         }
337         else
338         {
339             buf.append( "( " );
340 
341             for ( int ii = 0; ii < ocs.length; ii++ )
342             {
343                 if ( ii + 1 < ocs.length )
344                 {
345                     buf.append( ocs[ii].getName() ).append( " $ " );
346                 }
347                 else
348                 {
349                     buf.append( ocs[ii].getName() );
350                 }
351             }
352 
353             buf.append( " )" );
354         }
355 
356         return buf;
357     }
358 
359 
360     // ------------------------------------------------------------------------
361     // attributeType list rendering operations
362     // ------------------------------------------------------------------------
363 
364     /**
365      * Renders a list of attributeTypes for things like the must or may list of
366      * objectClasses using the ( oid $ oid ) format.
367      * 
368      * @param ats
369      *            the attributeTypes to list
370      * @return a buffer which contains the rendered list
371      */
372     public static StringBuilder render( AttributeType[] ats )
373     {
374         StringBuilder buf = new StringBuilder();
375         return render( buf, ats );
376     }
377 
378 
379     /**
380      * Renders a list of attributeTypes for things like the must or may list of
381      * objectClasses using the ( oid $ oid ) format into an existing buffer.
382      * 
383      * @param buf
384      *            the string buffer to render the list of attributeTypes into
385      * @param ats
386      *            the attributeTypes to list
387      * @return a buffer which contains the rendered list
388      */
389     public static StringBuilder render( StringBuilder buf, AttributeType[] ats )
390     {
391         if ( ats == null || ats.length == 0 )
392         {
393             return buf;
394         }
395         else if ( ats.length == 1 )
396         {
397             buf.append( ats[0].getName() );
398         }
399         else
400         {
401             buf.append( "( " );
402             for ( int ii = 0; ii < ats.length; ii++ )
403             {
404                 if ( ii + 1 < ats.length )
405                 {
406                     buf.append( ats[ii].getName() ).append( " $ " );
407                 }
408                 else
409                 {
410                     buf.append( ats[ii].getName() );
411                 }
412             }
413             buf.append( " )" );
414         }
415 
416         return buf;
417     }
418 
419 
420     // ------------------------------------------------------------------------
421     // schema object rendering operations
422     // ------------------------------------------------------------------------
423 
424     /**
425      * Renders the schema extensions into a new StringBuffer.
426      *
427      * @param extensions the schema extensions map with key and values
428      * @return a StringBuffer with the extensions component of a syntax description
429      */
430     public static StringBuilder render( Map<String, List<String>> extensions )
431     {
432         StringBuilder buf = new StringBuilder();
433 
434         if ( extensions.isEmpty() )
435         {
436             return buf;
437         }
438 
439         for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
440         {
441             buf.append( " " ).append( entry.getKey() ).append( " " );
442 
443             List<String> values = entry.getValue();
444 
445             // For extensions without values like X-IS-HUMAN-READIBLE
446             if ( values == null || values.isEmpty() )
447             {
448                 continue;
449             }
450 
451             // For extensions with a single value we can use one qdstring like 'value'
452             if ( values.size() == 1 )
453             {
454                 buf.append( "'" ).append( values.get( 0 ) ).append( "' " );
455                 continue;
456             }
457 
458             // For extensions with several values we have to surround whitespace
459             // separated list of qdstrings like ( 'value0' 'value1' 'value2' )
460             buf.append( "( " );
461             for ( String value : values )
462             {
463                 buf.append( "'" ).append( value ).append( "' " );
464             }
465             buf.append( ")" );
466         }
467 
468         if ( buf.charAt( buf.length() - 1 ) != ' ' )
469         {
470             buf.append( " " );
471         }
472 
473         return buf;
474     }
475 
476 
477     /**
478      * Returns a String description of a schema. The resulting String format is :
479      * <br>
480      * (OID [DESC '&lt;description&gt;'] FQCN &lt;fcqn&gt; [BYTECODE &lt;bytecode&gt;] X-SCHEMA '&lt;schema&gt;')
481      * <br>
482      * @param description The description to transform to a String
483      * @return The rendered schema object
484      */
485     public static String render( LoadableSchemaObject description )
486     {
487         StringBuilder buf = new StringBuilder();
488         buf.append( "( " ).append( description.getOid() );
489 
490         if ( description.getDescription() != null )
491         {
492             buf.append( " DESC " );
493             renderQDString( buf, description.getDescription() );
494         }
495 
496         buf.append( " FQCN " ).append( description.getFqcn() );
497 
498         if ( !Strings.isEmpty( description.getBytecode() ) )
499         {
500             buf.append( " BYTECODE " ).append( description.getBytecode() );
501         }
502 
503         buf.append( " X-SCHEMA '" );
504         buf.append( getSchemaName( description ) );
505         buf.append( "' )" );
506 
507         return buf.toString();
508     }
509 
510 
511     private static String getSchemaName( SchemaObject desc )
512     {
513         List<String> values = desc.getExtension( MetaSchemaConstants.X_SCHEMA_AT );
514 
515         if ( values == null || values.isEmpty() )
516         {
517             return MetaSchemaConstants.SCHEMA_OTHER;
518         }
519 
520         return values.get( 0 );
521     }
522 
523 
524     /**
525      * Remove the options from the attributeType, and returns the ID.
526      * <br>
527      * RFC 4512 :
528      * <pre>
529      * attributedescription = attributetype options
530      * attributetype = oid
531      * options = *( SEMI option )
532      * option = 1*keychar
533      * </pre>
534      * 
535      * @param attributeId The AttributeType to parse
536      * @return The AttributeType without its options
537      */
538     public static String stripOptions( String attributeId )
539     {
540         int optionsPos = attributeId.indexOf( ';' );
541 
542         if ( optionsPos != -1 )
543         {
544             return attributeId.substring( 0, optionsPos );
545         }
546         else
547         {
548             return attributeId;
549         }
550     }
551 
552 
553     /**
554      * Get the options from the attributeType.
555      * <br>
556      * For instance, given :
557      * jpegphoto;binary;lang=jp
558      * <br>
559      * your get back a set containing { "binary", "lang=jp" }
560      * 
561      * @param attributeId The AttributeType to parse
562      * @return a Set of options found for this AttributeType, or null
563      */
564     public static Set<String> getOptions( String attributeId )
565     {
566         int optionsPos = attributeId.indexOf( ';' );
567 
568         if ( optionsPos != -1 )
569         {
570             Set<String> options = new HashSet<>();
571 
572             String[] res = attributeId.substring( optionsPos + 1 ).split( ";" );
573 
574             for ( String option : res )
575             {
576                 if ( !Strings.isEmpty( option ) )
577                 {
578                     options.add( option );
579                 }
580             }
581 
582             return options;
583         }
584         else
585         {
586             return null;
587         }
588     }
589 
590 
591     /**
592      * Transform an UUID in a byte array
593      * @param uuid The UUID to transform
594      * @return The byte[] representing the UUID
595      */
596     public static byte[] uuidToBytes( UUID uuid )
597     {
598         Long low = uuid.getLeastSignificantBits();
599         Long high = uuid.getMostSignificantBits();
600         byte[] bytes = new byte[16];
601 
602         bytes[0] = ( byte ) ( ( high & 0xff00000000000000L ) >> 56 );
603         bytes[1] = ( byte ) ( ( high & 0x00ff000000000000L ) >> 48 );
604         bytes[2] = ( byte ) ( ( high & 0x0000ff0000000000L ) >> 40 );
605         bytes[3] = ( byte ) ( ( high & 0x000000ff00000000L ) >> 32 );
606         bytes[4] = ( byte ) ( ( high & 0x00000000ff000000L ) >> 24 );
607         bytes[5] = ( byte ) ( ( high & 0x0000000000ff0000L ) >> 16 );
608         bytes[6] = ( byte ) ( ( high & 0x000000000000ff00L ) >> 8 );
609         bytes[7] = ( byte ) ( high & 0x00000000000000ffL );
610         bytes[8] = ( byte ) ( ( low & 0xff00000000000000L ) >> 56 );
611         bytes[9] = ( byte ) ( ( low & 0x00ff000000000000L ) >> 48 );
612         bytes[10] = ( byte ) ( ( low & 0x0000ff0000000000L ) >> 40 );
613         bytes[11] = ( byte ) ( ( low & 0x000000ff00000000L ) >> 32 );
614         bytes[12] = ( byte ) ( ( low & 0x00000000ff000000L ) >> 24 );
615         bytes[13] = ( byte ) ( ( low & 0x0000000000ff0000L ) >> 16 );
616         bytes[14] = ( byte ) ( ( low & 0x000000000000ff00L ) >> 8 );
617         bytes[15] = ( byte ) ( low & 0x00000000000000ffL );
618 
619         return bytes;
620     }
621 
622 
623     /**
624      * Tells if an AttributeType name is valid or not. An Attribute name is valid if 
625      * it's a descr / numericoid, as described in rfc4512 :
626      * <pre>
627      * name = descr / numericOid
628      * descr = keystring
629      * keystring = leadkeychar *keychar
630      * leadkeychar = ALPHA
631      * keychar = ALPHA / DIGIT / HYPHEN / USCORE
632      * numericoid = number 1*( DOT number )
633      * number  = DIGIT / ( LDIGIT 1*DIGIT )
634      * ALPHA   = %x41-5A / %x61-7A   ; "A"-"Z" / "a"-"z"
635      * DIGIT   = %x30 / LDIGIT       ; "0"-"9"
636      * HYPHEN  = %x2D ; hyphen ("-")
637      * LDIGIT  = %x31-39             ; "1"-"9"
638      * DOT     = %x2E ; period (".")
639      * USCORE  = %x5F ; underscore ("_")
640      * </pre>
641      * 
642      * Note that we have extended this grammar to accept the '_' char, which is widely used in teh LDAP world.
643      *
644      * @param attributeName The AttributeType name to check
645      * @return true if it's valid
646      */
647     public static boolean isAttributeNameValid( String attributeName )
648     {
649         if ( Strings.isEmpty( attributeName ) )
650         {
651             return false;
652         }
653         
654         // Check the first char which must be ALPHA or DIGIT
655         boolean descr;
656         boolean zero = false;
657         boolean dot = false;
658         
659         char c = attributeName.charAt( 0 );
660         
661         if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) )
662         {
663             descr = true;
664         }
665         else if ( ( c >= '0' ) && ( c <= '9' ) )
666         {
667             descr = false;
668             
669             zero = c == '0'; 
670         }
671         else
672         {
673             return false;
674         }
675         
676         for ( int i = 1; i < attributeName.length(); i++ )
677         {
678             c = attributeName.charAt( i ); 
679             
680             if ( descr )
681             {
682                 // This is a descr, iterate on KeyChars (ALPHA / DIGIT / HYPHEN / USCORE)
683                 if ( ( ( c < 'a' ) || ( c > 'z' ) )
684                     && ( ( c < 'A' ) || ( c > 'Z' ) )
685                     && ( ( c < '0' ) || ( c > '9' ) )
686                     && ( c != '-' )
687                     && ( c != '_' ) )
688                 {
689                     return false;
690                 }
691             }
692             else
693             {
694                 // This is a numericOid, check it
695                 if ( c == '.' )
696                 {
697                     // Not allowed if we already have had a dot
698                     if ( dot )
699                     {
700                         return false;
701                     }
702                     
703                     dot = true;
704                     zero = false;
705                 }
706                 else if ( ( c >= '0' ) && ( c <= '9' ) )
707                 {
708                     dot = false;
709                     
710                     if ( zero )
711                     {
712                         // We can't have a leading '0' followed by another number
713                         return false;
714                     }
715                     else if ( c == '0' )
716                     {
717                         zero = true;
718                     }
719                 }
720                 else
721                 {
722                     // Not valid
723                     return false;
724                 }
725             }
726         }
727         
728         return true;
729     }
730 }