001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 * 
010 *    https://www.apache.org/licenses/LICENSE-2.0
011 * 
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 * 
019 */
020package org.apache.directory.api.ldap.model.schema;
021
022
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.UUID;
028
029import org.apache.directory.api.i18n.I18n;
030import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
031import org.apache.directory.api.ldap.model.constants.SchemaConstants;
032import org.apache.directory.api.ldap.model.entry.Attribute;
033import org.apache.directory.api.ldap.model.entry.Entry;
034import org.apache.directory.api.ldap.model.entry.Modification;
035import org.apache.directory.api.ldap.model.entry.Value;
036import org.apache.directory.api.ldap.model.exception.LdapException;
037import org.apache.directory.api.util.Strings;
038
039
040/**
041 * Various utility methods for schema functions and objects.
042 * 
043 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
044 */
045public final class SchemaUtils
046{
047    /**
048     * Private constructor.
049     */
050    private SchemaUtils()
051    {
052    }
053
054
055    /**
056     * Gets the target entry as it would look after a modification operation
057     * were performed on it.
058     * 
059     * @param mods the modifications performed on the entry
060     * @param entry the source entry that is modified
061     * @return the resultant entry after the modifications have taken place
062     * @throws LdapException if there are problems accessing attributes
063     */
064    public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry )
065        throws LdapException
066    {
067        Entry targetEntry = entry.clone();
068
069        for ( Modification mod : mods )
070        {
071            String id = mod.getAttribute().getId();
072
073            switch ( mod.getOperation() )
074            {
075                case REPLACE_ATTRIBUTE:
076                    targetEntry.put( mod.getAttribute() );
077                    break;
078
079                case ADD_ATTRIBUTE:
080                    Attribute combined = mod.getAttribute().clone();
081                    Attribute toBeAdded = mod.getAttribute();
082                    Attribute existing = entry.get( id );
083
084                    if ( existing != null )
085                    {
086                        for ( Value value : existing )
087                        {
088                            combined.add( value );
089                        }
090                    }
091
092                    for ( Value value : toBeAdded )
093                    {
094                        combined.add( value );
095                    }
096
097                    targetEntry.put( combined );
098                    break;
099
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}