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