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.io.Serializable;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.apache.directory.api.i18n.I18n;
033import org.apache.directory.api.util.Strings;
034
035
036/**
037 * Most schema objects have some common attributes. This class
038 * contains the minimum set of properties exposed by a SchemaObject.<br>
039 * We have 11 types of SchemaObjects :
040 * <ul>
041 *   <li> AttributeType</li>
042 *   <li> DitCOntentRule</li>
043 *   <li> DitStructureRule</li>
044 *   <li> LdapComparator (specific to ADS)</li>
045 *   <li> LdapSyntaxe</li>
046 *   <li> MatchingRule</li>
047 *   <li> MatchingRuleUse</li>
048 *   <li> NameForm</li>
049 *   <li> Normalizer (specific to ADS)</li>
050 *   <li> ObjectClass</li>
051 *   <li> SyntaxChecker (specific to ADS)</li>
052 * </ul>
053 * <br>
054 * <br>
055 * This class provides accessors and setters for the following attributes,
056 * which are common to all those SchemaObjects :
057 * <ul>
058 *  <li>oid : The numeric OID</li>
059 *   <li>description : The SchemaObject description</li>
060 *   <li>obsolete : Tells if the schema object is obsolete</li>
061 *   <li>extensions : The extensions, a key/Values map</li>
062 *   <li>schemaObjectType : The SchemaObject type (see upper)</li>
063 *   <li>schema : The schema the SchemaObject is associated with (it's an extension).
064 *     Can be null</li>
065 *   <li>isEnabled : The SchemaObject status (it's related to the schema status)</li>
066 *   <li>isReadOnly : Tells if the SchemaObject can be modified or not</li>
067 * </ul>
068 * <br><br>
069 * Some of those attributes are not used by some Schema elements, even if they should
070 * have been used. Here is the list :
071 * <ul>
072 *   <li><b>name</b> : LdapSyntax, Comparator, Normalizer, SyntaxChecker</li>
073 *   <li><b>numericOid</b> : DitStructureRule</li>
074 *   <li><b>obsolete</b> : LdapSyntax, Comparator, Normalizer, SyntaxChecker</li>
075 * </ul>
076 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
077 */
078public abstract class AbstractSchemaObject implements SchemaObject, Serializable
079{
080    /** The serial version UID */
081    private static final long serialVersionUID = 2L;
082
083    /** The SchemaObject numeric OID */
084    protected String oid;
085
086    /** The optional names for this SchemaObject */
087    protected transient List<String> names;
088
089    /** Whether or not this SchemaObject is enabled */
090    protected boolean isEnabled = true;
091
092    /** Whether or not this SchemaObject is obsolete */
093    protected boolean isObsolete = false;
094
095    /** A short description of this SchemaObject */
096    protected String description;
097
098    /** The SchemaObject specification */
099    protected String specification;
100
101    /** The name of the schema this object is associated with */
102    protected String schemaName;
103
104    /** The SchemaObjectType */
105    protected SchemaObjectType objectType;
106
107    /** A map containing the list of supported extensions */
108    protected transient Map<String, List<String>> extensions;
109
110    /** A locked to avoid modifications when set to true */
111    protected volatile boolean locked;
112
113    /** The hashcode for this schemaObject */
114    protected volatile int h = 0;
115
116
117    /**
118     * A constructor for a SchemaObject instance. It must be
119     * invoked by the inherited class.
120     *
121     * @param objectType The SchemaObjectType to create
122     * @param oid the SchemaObject numeric OID
123     */
124    protected AbstractSchemaObject( SchemaObjectType objectType, String oid )
125    {
126        this.objectType = objectType;
127        this.oid = oid;
128        extensions = new HashMap<>();
129        names = new ArrayList<>();
130        computeHashCode();
131    }
132
133
134    /**
135     * Constructor used when a generic reusable SchemaObject is assigned an
136     * OID after being instantiated.
137     * 
138     * @param objectType The SchemaObjectType to create
139     */
140    protected AbstractSchemaObject( SchemaObjectType objectType )
141    {
142        this.objectType = objectType;
143        extensions = new HashMap<>();
144        names = new ArrayList<>();
145    }
146
147
148    /**
149     * Gets usually what is the numeric object identifier assigned to this
150     * SchemaObject. All schema objects except for MatchingRuleUses have an OID
151     * assigned specifically to then. A MatchingRuleUse's OID really is the OID
152     * of it's MatchingRule and not specific to the MatchingRuleUse. This
153     * effects how MatchingRuleUse objects are maintained by the system.
154     * 
155     * @return an OID for this SchemaObject or its MatchingRule if this
156     *         SchemaObject is a MatchingRuleUse object
157     */
158    @Override
159    public String getOid()
160    {
161        return oid;
162    }
163
164
165    /**
166     * A special method used when renaming an SchemaObject: we may have to
167     * change it's OID
168     * @param oid The new OID
169     */
170    @Override
171    public void setOid( String oid )
172    {
173        if ( locked )
174        {
175            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
176        }
177
178        this.oid = oid;
179        
180        computeHashCode();
181    }
182
183
184    /**
185     * Gets short names for this SchemaObject if any exists for it, otherwise,
186     * returns an empty list.
187     * 
188     * @return the names for this SchemaObject
189     */
190    @Override
191    public List<String> getNames()
192    {
193        if ( names != null )
194        {
195            return Collections.unmodifiableList( names );
196        }
197        else
198        {
199            return Collections.emptyList();
200        }
201    }
202
203
204    /**
205     * Gets the first name in the set of short names for this SchemaObject if
206     * any exists for it.
207     * 
208     * @return the first of the names for this SchemaObject or the oid
209     * if one does not exist
210     */
211    @Override
212    public String getName()
213    {
214        if ( ( names != null ) && !names.isEmpty() )
215        {
216            return names.get( 0 );
217        }
218        else
219        {
220            return oid;
221        }
222    }
223
224
225    /**
226     * Add a new name to the list of names for this SchemaObject. The name
227     * is lower cased and trimmed.
228     * 
229     * @param namesToAdd The names to add
230     */
231    @Override
232    public void addName( String... namesToAdd )
233    {
234        if ( locked )
235        {
236            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
237        }
238
239        // We must avoid duplicated names, as names are case insensitive
240        Set<String> lowerNames = new HashSet<>();
241
242        // Fills a set with all the existing names
243        for ( String name : this.names )
244        {
245            lowerNames.add( Strings.toLowerCaseAscii( name ) );
246        }
247
248        for ( String name : namesToAdd )
249        {
250            if ( name != null )
251            {
252                String lowerName = Strings.toLowerCaseAscii( name );
253                // Check that the lower cased names is not already present
254                if ( !lowerNames.contains( lowerName ) )
255                {
256                    this.names.add( name );
257                    lowerNames.add( lowerName );
258                }
259            }
260        }
261        
262        computeHashCode();
263    }
264
265
266    /**
267     * Sets the list of names for this SchemaObject. The names are
268     * lowercased and trimmed.
269     * 
270     * @param names The list of names. Can be empty
271     */
272    @Override
273    public void setNames( List<String> names )
274    {
275        if ( locked )
276        {
277            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
278        }
279
280        if ( names == null )
281        {
282            return;
283        }
284
285        this.names = new ArrayList<>( names.size() );
286
287        for ( String name : names )
288        {
289            if ( name != null )
290            {
291                this.names.add( name );
292            }
293        }
294        
295        computeHashCode();
296    }
297
298
299    /**
300     * Sets the list of names for this SchemaObject. The names are
301     * lowercased and trimmed.
302     * 
303     * @param names The list of names.
304     */
305    public void setNames( String... names )
306    {
307        if ( locked )
308        {
309            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
310        }
311
312        if ( names == null )
313        {
314            return;
315        }
316
317        this.names.clear();
318
319        for ( String name : names )
320        {
321            if ( name != null )
322            {
323                this.names.add( name );
324            }
325        }
326        
327        computeHashCode();
328    }
329
330
331    /**
332     * Gets a short description about this SchemaObject.
333     * 
334     * @return a short description about this SchemaObject
335     */
336    @Override
337    public String getDescription()
338    {
339        return description;
340    }
341
342
343    /**
344     * Sets the SchemaObject's description
345     * 
346     * @param description The SchemaObject's description
347     */
348    @Override
349    public void setDescription( String description )
350    {
351        if ( locked )
352        {
353            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
354        }
355
356        this.description = description;
357        
358        computeHashCode();
359    }
360
361
362    /**
363     * Gets the SchemaObject specification.
364     * 
365     * @return the SchemaObject specification
366     */
367    @Override
368    public String getSpecification()
369    {
370        return specification;
371    }
372
373
374    /**
375     * Sets the SchemaObject's specification
376     * 
377     * @param specification The SchemaObject's specification
378     */
379    @Override
380    public void setSpecification( String specification )
381    {
382        if ( locked )
383        {
384            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
385        }
386
387        this.specification = specification;
388        
389        computeHashCode();
390    }
391
392
393    /**
394     * Tells if this SchemaObject is enabled.
395     * 
396     * @return true if the SchemaObject is enabled, or if it depends on
397     * an enabled schema
398     */
399    @Override
400    public boolean isEnabled()
401    {
402        return isEnabled;
403    }
404
405
406    /**
407     * Tells if this SchemaObject is disabled.
408     * 
409     * @return true if the SchemaObject is disabled
410     */
411    @Override
412    public boolean isDisabled()
413    {
414        return !isEnabled;
415    }
416
417
418    /**
419     * Sets the SchemaObject state, either enabled or disabled.
420     * 
421     * @param enabled The current SchemaObject state
422     */
423    @Override
424    public void setEnabled( boolean enabled )
425    {
426        if ( locked )
427        {
428            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
429        }
430
431        isEnabled = enabled;
432        
433        computeHashCode();
434    }
435
436
437    /**
438     * Gets whether or not this SchemaObject has been inactivated. All
439     * SchemaObjects except Syntaxes allow for this parameter within their
440     * definition. For Syntaxes this property should always return false in
441     * which case it is never included in the description.
442     * 
443     * @return true if inactive, false if active
444     */
445    @Override
446    public boolean isObsolete()
447    {
448        return isObsolete;
449    }
450
451
452    /**
453     * Sets the Obsolete flag.
454     * 
455     * @param obsolete The Obsolete flag state
456     */
457    @Override
458    public void setObsolete( boolean obsolete )
459    {
460        if ( locked )
461        {
462            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
463        }
464
465        this.isObsolete = obsolete;
466        
467        computeHashCode();
468    }
469
470
471    /**
472     * {@inheritDoc}
473     */
474    @Override
475    public Map<String, List<String>> getExtensions()
476    {
477        return extensions;
478    }
479
480
481    /**
482     * {@inheritDoc}
483     */
484    @Override
485    public boolean hasExtension( String extension )
486    {
487        return extensions.containsKey( Strings.toUpperCaseAscii( extension ) );
488    }
489
490
491    /**
492     * {@inheritDoc}
493     */
494    @Override
495    public List<String> getExtension( String extension )
496    {
497        String name = Strings.toUpperCaseAscii( extension );
498        
499        if ( hasExtension( name ) )
500        {
501            for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
502            {
503                String key = entry.getKey();
504                
505                if ( name.equalsIgnoreCase( key ) )
506                {
507                    return entry.getValue();
508                }
509            }
510        }
511
512        return null;
513    }
514
515
516    /**
517     * Add an extension with its values
518     * @param key The extension key
519     * @param values The associated values
520     */
521    @Override
522    public void addExtension( String key, String... values )
523    {
524        if ( locked )
525        {
526            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
527        }
528
529        List<String> valueList = new ArrayList<>();
530
531        for ( String value : values )
532        {
533            valueList.add( value );
534        }
535
536        extensions.put( Strings.toUpperCaseAscii( key ), valueList );
537        
538        computeHashCode();
539    }
540
541
542    /**
543     * Add an extension with its values
544     * @param key The extension key
545     * @param values The associated values
546     */
547    @Override
548    public void addExtension( String key, List<String> values )
549    {
550        if ( locked )
551        {
552            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
553        }
554
555        extensions.put( Strings.toUpperCaseAscii( key ), values );
556        
557        computeHashCode();
558    }
559
560
561    /**
562     * Add an extensions with their values. (Actually do a copy)
563     * 
564     * @param extensions The extensions map
565     */
566    @Override
567    public void setExtensions( Map<String, List<String>> extensions )
568    {
569        if ( locked )
570        {
571            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
572        }
573
574        if ( extensions != null )
575        {
576            this.extensions = new HashMap<>();
577
578            for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
579            {
580                List<String> values = new ArrayList<>();
581
582                for ( String value : entry.getValue() )
583                {
584                    values.add( value );
585                }
586
587                this.extensions.put( Strings.toUpperCaseAscii( entry.getKey() ), values );
588            }
589
590            computeHashCode();
591        }
592    }
593
594
595    /**
596     * The SchemaObject type :
597     * <ul>
598     *   <li> AttributeType
599     *   <li> DitCOntentRule
600     *   <li> DitStructureRule
601     *   <li> LdapComparator (specific to ADS)
602     *   <li> LdapSyntaxe
603     *   <li> MatchingRule
604     *   <li> MatchingRuleUse
605     *   <li> NameForm
606     *   <li> Normalizer (specific to ADS)
607     *   <li> ObjectClass
608     *   <li> SyntaxChecker (specific to ADS)
609     * </ul>
610     * 
611     * @return the SchemaObject type
612     */
613    @Override
614    public SchemaObjectType getObjectType()
615    {
616        return objectType;
617    }
618
619
620    /**
621     * Gets the name of the schema this SchemaObject is associated with.
622     *
623     * @return the name of the schema associated with this schemaObject
624     */
625    @Override
626    public String getSchemaName()
627    {
628        return schemaName;
629    }
630
631
632    /**
633     * Sets the name of the schema this SchemaObject is associated with.
634     * 
635     * @param schemaName the new schema name
636     */
637    @Override
638    public void setSchemaName( String schemaName )
639    {
640        if ( locked )
641        {
642            throw new UnsupportedOperationException( I18n.err( I18n.ERR_13700_CANNOT_MODIFY_LOCKED_SCHEMA_OBJECT, getName() ) );
643        }
644
645        this.schemaName = schemaName;
646        
647        computeHashCode();
648    }
649
650
651    /**
652     * {@inheritDoc}
653     */
654    @Override
655    public boolean equals( Object o1 )
656    {
657        if ( this == o1 )
658        {
659            return true;
660        }
661
662        if ( !( o1 instanceof AbstractSchemaObject ) )
663        {
664            return false;
665        }
666
667        AbstractSchemaObject that = ( AbstractSchemaObject ) o1;
668
669        // Two schemaObject are equals if their oid is equal,
670        // their ObjectType is equal, their names are equals
671        // their schema name is the same, all their flags are equals,
672        // the description is the same and their extensions are equals
673        if ( !compareOid( oid, that.oid ) )
674        {
675            return false;
676        }
677
678        // Compare the names
679        if ( names == null )
680        {
681            if ( that.names != null )
682            {
683                return false;
684            }
685        }
686        else if ( that.names == null )
687        {
688            return false;
689        }
690        else
691        {
692            int nbNames = 0;
693
694            for ( String name : names )
695            {
696                if ( !that.names.contains( name ) )
697                {
698                    return false;
699                }
700
701                nbNames++;
702            }
703
704            if ( nbNames != names.size() )
705            {
706                return false;
707            }
708        }
709
710        if ( schemaName == null )
711        {
712            if ( that.schemaName != null )
713            {
714                return false;
715            }
716        }
717        else
718        {
719            if ( !schemaName.equalsIgnoreCase( that.schemaName ) )
720            {
721                return false;
722            }
723        }
724
725        if ( objectType != that.objectType )
726        {
727            return false;
728        }
729
730        if ( extensions != null )
731        {
732            if ( that.extensions == null )
733            {
734                return false;
735            }
736            else
737            {
738                for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
739                {
740                    String key = entry.getKey();
741                    
742                    if ( !that.extensions.containsKey( key ) )
743                    {
744                        return false;
745                    }
746
747                    List<String> thisValues = entry.getValue();
748                    List<String> thatValues = that.extensions.get( key );
749
750                    if ( thisValues != null )
751                    {
752                        if ( thatValues == null )
753                        {
754                            return false;
755                        }
756                        else
757                        {
758                            if ( thisValues.size() != thatValues.size() )
759                            {
760                                return false;
761                            }
762
763                            // TODO compare the values
764                        }
765                    }
766                    else if ( thatValues != null )
767                    {
768                        return false;
769                    }
770                }
771            }
772        }
773        else if ( that.extensions != null )
774        {
775            return false;
776        }
777
778        if ( this.isEnabled != that.isEnabled )
779        {
780            return false;
781        }
782
783        if ( this.isObsolete != that.isObsolete )
784        {
785            return false;
786        }
787
788        if ( this.description == null )
789        {
790            return that.description == null;
791        }
792        else
793        {
794            return this.description.equalsIgnoreCase( that.description );
795        }
796    }
797
798
799    /**
800     * Compare two oids, and return true if they are both null or equal.
801     *
802     * @param oid1 the first OID
803     * @param oid2 the second OID
804     * @return <code>true</code> if both OIDs are null or equal
805     */
806    protected boolean compareOid( String oid1, String oid2 )
807    {
808        if ( oid1 == null )
809        {
810            return oid2 == null;
811        }
812        else
813        {
814            return oid1.equals( oid2 );
815        }
816    }
817
818
819    /**
820     * {@inheritDoc}
821     */
822    @Override
823    public SchemaObject copy( SchemaObject original )
824    {
825        // copy the description
826        description = original.getDescription();
827
828        // copy the flags
829        isEnabled = original.isEnabled();
830        isObsolete = original.isObsolete();
831
832        // copy the names
833        names = new ArrayList<>();
834
835        for ( String name : original.getNames() )
836        {
837            names.add( name );
838        }
839
840        // copy the extensions
841        extensions = new HashMap<>();
842
843        for ( String key : original.getExtensions().keySet() )
844        {
845            List<String> extensionValues = original.getExtension( key );
846
847            List<String> cloneExtension = new ArrayList<>();
848
849            for ( String value : extensionValues )
850            {
851                cloneExtension.add( value );
852            }
853
854            extensions.put( key, cloneExtension );
855        }
856
857        // The SchemaName
858        schemaName = original.getSchemaName();
859
860        // The specification
861        specification = original.getSpecification();
862
863        return this;
864    }
865
866
867    /**
868     * Clear the current SchemaObject : remove all the references to other objects,
869     * and all the Maps.
870     */
871    @Override
872    public void clear()
873    {
874        // Clear the extensions
875        for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
876        {
877            List<String> extensionList = entry.getValue();
878
879            extensionList.clear();
880        }
881
882        extensions.clear();
883
884        // Clear the names
885        names.clear();
886        
887        computeHashCode();
888    }
889
890
891    /**
892     * {@inheritDoc}
893     */
894    @Override
895    public final void lock()
896    {
897        locked = true;
898    }
899
900
901    /**
902     * Unlock the Schema Object and make it modifiable again.
903     */
904    public void unlock()
905    {
906        locked = false;
907    }
908    
909    
910    /**
911     * Compute the hashcode, and store it in the 'h' variable
912     */
913    protected void computeHashCode()
914    {
915        int hash = 37;
916
917        // The OID
918        if ( oid != null )
919        {
920            hash += hash * 17 + oid.hashCode();
921        }
922
923        // The SchemaObject type
924        if ( objectType != null )
925        {
926            hash += hash * 17 + objectType.getValue();
927        }
928
929        // The Names, if any
930        if ( ( names != null ) && !names.isEmpty() )
931        {
932            int tempHash = 0;
933            
934            for ( String name : names )
935            {
936                tempHash *= name.hashCode();
937            }
938            
939            hash = hash * 17 + tempHash;
940        }
941
942        // The schemaName if any
943        if ( schemaName != null )
944        {
945            hash += hash * 17 + schemaName.hashCode();
946        }
947
948        hash += hash * 17 + ( isEnabled ? 1 : 0 );
949
950        // The description, if any
951        if ( description != null )
952        {
953            hash += hash * 17 + description.hashCode();
954        }
955
956        // The extensions, if any
957        // Because the extensions and their values are stored un-ordered
958        // we have to be careful when computing the hashcode so that it does
959        // not depend on the extensions/values order
960        int tempHash = 0;
961        
962        for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
963        {
964            String key = entry.getKey();
965            int tempHash2 = key.hashCode();
966
967            List<String> values = entry.getValue();
968
969            if ( values != null )
970            {
971                int tempHash3 = 0;
972                
973                for ( String value : values )
974                {
975                    tempHash3 += value.hashCode();
976                }
977
978                tempHash += tempHash2 * tempHash3;
979            }
980        }
981        
982        hash = hash * 17 + tempHash;
983        
984        h = hash;
985    }
986
987
988    /**
989     * {@inheritDoc}
990     */
991    @Override
992    public int hashCode()
993    {
994        return h;
995    }
996}