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