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 *    http://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.entry.Attribute;
032import org.apache.directory.api.ldap.model.entry.Entry;
033import org.apache.directory.api.ldap.model.entry.Modification;
034import org.apache.directory.api.ldap.model.entry.Value;
035import org.apache.directory.api.ldap.model.exception.LdapException;
036import org.apache.directory.api.util.Strings;
037
038
039/**
040 * Various utility methods for schema functions and objects.
041 * 
042 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
043 */
044public final class SchemaUtils
045{
046    /**
047     * Private constructor.
048     */
049    private SchemaUtils()
050    {
051    }
052
053
054    /**
055     * Gets the target entry as it would look after a modification operation
056     * were performed on it.
057     * 
058     * @param mods the modifications performed on the entry
059     * @param entry the source entry that is modified
060     * @return the resultant entry after the modifications have taken place
061     * @throws LdapException if there are problems accessing attributes
062     */
063    public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry )
064        throws LdapException
065    {
066        Entry targetEntry = entry.clone();
067
068        for ( Modification mod : mods )
069        {
070            String id = mod.getAttribute().getId();
071
072            switch ( mod.getOperation() )
073            {
074                case REPLACE_ATTRIBUTE:
075                    targetEntry.put( mod.getAttribute() );
076                    break;
077
078                case ADD_ATTRIBUTE:
079                    Attribute combined = mod.getAttribute().clone();
080                    Attribute toBeAdded = mod.getAttribute();
081                    Attribute existing = entry.get( id );
082
083                    if ( existing != null )
084                    {
085                        for ( Value<?> value : existing )
086                        {
087                            combined.add( value );
088                        }
089                    }
090
091                    for ( Value<?> value : toBeAdded )
092                    {
093                        combined.add( value );
094                    }
095
096                    targetEntry.put( combined );
097                    break;
098
099                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}