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     * {@inheritDoc}
481     */
482    public Map<String, List<String>> getExtensions()
483    {
484        return extensions;
485    }
486
487
488    /**
489     * {@inheritDoc}
490     */
491    public boolean hasExtension( String extension )
492    {
493        return extensions.containsKey( Strings.toUpperCase( extension ) );
494    }
495
496
497    /**
498     * {@inheritDoc}
499     */
500    public List<String> getExtension( String extension )
501    {
502        String name = Strings.toUpperCase( extension );
503
504        if ( hasExtension( name ) )
505        {
506            for ( String key : extensions.keySet() )
507            {
508                if ( name.equalsIgnoreCase( key ) )
509                {
510                    return extensions.get( key );
511                }
512            }
513        }
514
515        return null;
516    }
517
518
519    /**
520     * Add an extension with its values
521     * @param key The extension key
522     * @param values The associated values
523     */
524    public void addExtension( String key, String... values )
525    {
526        if ( locked )
527        {
528            throw new UnsupportedOperationException( I18n.err( I18n.ERR_04441, getName() ) );
529        }
530
531        if ( !isReadOnly )
532        {
533            List<String> valueList = new ArrayList<String>();
534
535            for ( String value : values )
536            {
537                valueList.add( value );
538            }
539
540            extensions.put( Strings.toUpperCase( key ), valueList );
541        }
542    }
543
544
545    /**
546     * Add an extension with its values
547     * @param key The extension key
548     * @param values The associated values
549     */
550    public void addExtension( String key, List<String> values )
551    {
552        if ( locked )
553        {
554            throw new UnsupportedOperationException( I18n.err( I18n.ERR_04441, getName() ) );
555        }
556
557        if ( !isReadOnly )
558        {
559            extensions.put( Strings.toUpperCase( key ), values );
560        }
561    }
562
563
564    /**
565     * Add an extensions with their values. (Actually do a copy)
566     * 
567     * @param extensions The extensions map
568     */
569    public void setExtensions( Map<String, List<String>> extensions )
570    {
571        if ( locked )
572        {
573            throw new UnsupportedOperationException( I18n.err( I18n.ERR_04441, getName() ) );
574        }
575
576        if ( !isReadOnly && ( extensions != null ) )
577        {
578            this.extensions = new HashMap<String, List<String>>();
579
580            for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
581            {
582                List<String> values = new ArrayList<String>();
583
584                for ( String value : entry.getValue() )
585                {
586                    values.add( value );
587                }
588
589                this.extensions.put( Strings.toUpperCase( entry.getKey() ), values );
590            }
591
592        }
593    }
594
595
596    /**
597     * The SchemaObject type :
598     * <ul>
599     *   <li> AttributeType
600     *   <li> DitCOntentRule
601     *   <li> DitStructureRule
602     *   <li> LdapComparator (specific to ADS)
603     *   <li> LdapSyntaxe
604     *   <li> MatchingRule
605     *   <li> MatchingRuleUse
606     *   <li> NameForm
607     *   <li> Normalizer (specific to ADS)
608     *   <li> ObjectClass
609     *   <li> SyntaxChecker (specific to ADS)
610     * </ul>
611     * 
612     * @return the SchemaObject type
613     */
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    public String getSchemaName()
626    {
627        return schemaName;
628    }
629
630
631    /**
632     * Sets the name of the schema this SchemaObject is associated with.
633     * 
634     * @param schemaName the new schema name
635     */
636    public void setSchemaName( String schemaName )
637    {
638        if ( locked )
639        {
640            throw new UnsupportedOperationException( I18n.err( I18n.ERR_04441, getName() ) );
641        }
642
643        if ( !isReadOnly )
644        {
645            this.schemaName = schemaName;
646        }
647    }
648
649
650    /**
651     * This method is final to forbid the inherited classes to implement
652     * it. This has been done for performances reasons : the hashcode should
653     * be computed only once, and stored locally.
654     * 
655     * The hashcode is currently computed in the lock() method, which is a hack
656     * that should be fixed.
657     * 
658     * @return {@inheritDoc}
659     */
660    @Override
661    public final int hashCode()
662    {
663        return h;
664    }
665
666
667    /**
668     * @{@inheritDoc}
669     */
670    @Override
671    public boolean equals( Object o1 )
672    {
673        if ( this == o1 )
674        {
675            return true;
676        }
677
678        if ( !( o1 instanceof AbstractSchemaObject ) )
679        {
680            return false;
681        }
682
683        AbstractSchemaObject that = ( AbstractSchemaObject ) o1;
684
685        // Two schemaObject are equals if their oid is equal,
686        // their ObjectType is equal, their names are equals
687        // their schema name is the same, all their flags are equals,
688        // the description is the same and their extensions are equals
689        if ( !compareOid( oid, that.oid ) )
690        {
691            return false;
692        }
693
694        // Compare the names
695        if ( names == null )
696        {
697            if ( that.names != null )
698            {
699                return false;
700            }
701        }
702        else if ( that.names == null )
703        {
704            return false;
705        }
706        else
707        {
708            int nbNames = 0;
709
710            for ( String name : names )
711            {
712                if ( !that.names.contains( name ) )
713                {
714                    return false;
715                }
716
717                nbNames++;
718            }
719
720            if ( nbNames != names.size() )
721            {
722                return false;
723            }
724        }
725
726        if ( schemaName == null )
727        {
728            if ( that.schemaName != null )
729            {
730                return false;
731            }
732        }
733        else
734        {
735            if ( !schemaName.equalsIgnoreCase( that.schemaName ) )
736            {
737                return false;
738            }
739        }
740
741        if ( objectType != that.objectType )
742        {
743            return false;
744        }
745
746        if ( extensions != null )
747        {
748            if ( that.extensions == null )
749            {
750                return false;
751            }
752            else
753            {
754                for ( String key : extensions.keySet() )
755                {
756                    if ( !that.extensions.containsKey( key ) )
757                    {
758                        return false;
759                    }
760
761                    List<String> thisValues = extensions.get( key );
762                    List<String> thatValues = that.extensions.get( key );
763
764                    if ( thisValues != null )
765                    {
766                        if ( thatValues == null )
767                        {
768                            return false;
769                        }
770                        else
771                        {
772                            if ( thisValues.size() != thatValues.size() )
773                            {
774                                return false;
775                            }
776
777                            // TODO compare the values
778                        }
779                    }
780                    else if ( thatValues != null )
781                    {
782                        return false;
783                    }
784                }
785            }
786        }
787        else if ( that.extensions != null )
788        {
789            return false;
790        }
791
792        if ( this.isEnabled != that.isEnabled )
793        {
794            return false;
795        }
796
797        if ( this.isObsolete != that.isObsolete )
798        {
799            return false;
800        }
801
802        if ( this.isReadOnly != that.isReadOnly )
803        {
804            return false;
805        }
806
807        if ( this.description == null )
808        {
809            return that.description == null;
810        }
811        else
812        {
813            return this.description.equalsIgnoreCase( that.description );
814        }
815    }
816
817
818    /**
819     * Copy the current SchemaObject on place
820     *
821     * @return The copied SchemaObject
822     */
823    public abstract SchemaObject copy();
824
825
826    /**
827     * Compare two oids, and return true if they are both null or equal.
828     *
829     * @param oid1 the first OID
830     * @param oid2 the second OID
831     * @return <code>true</code> if both OIDs are null or equal
832     */
833    protected boolean compareOid( String oid1, String oid2 )
834    {
835        if ( oid1 == null )
836        {
837            return oid2 == null;
838        }
839        else
840        {
841            return oid1.equals( oid2 );
842        }
843    }
844
845
846    /**
847     * {@inheritDoc}
848     */
849    public SchemaObject copy( SchemaObject original )
850    {
851        // copy the description
852        description = original.getDescription();
853
854        // copy the flags
855        isEnabled = original.isEnabled();
856        isObsolete = original.isObsolete();
857        isReadOnly = original.isReadOnly();
858
859        // copy the names
860        names = new ArrayList<String>();
861
862        for ( String name : original.getNames() )
863        {
864            names.add( name );
865        }
866
867        // copy the extensions
868        extensions = new HashMap<String, List<String>>();
869
870        for ( String key : original.getExtensions().keySet() )
871        {
872            List<String> extensionValues = original.getExtension( key );
873
874            List<String> cloneExtension = new ArrayList<String>();
875
876            for ( String value : extensionValues )
877            {
878                cloneExtension.add( value );
879            }
880
881            extensions.put( key, cloneExtension );
882        }
883
884        // The SchemaName
885        schemaName = original.getSchemaName();
886
887        // The specification
888        specification = original.getSpecification();
889
890        return this;
891    }
892
893
894    /**
895     * Clear the current SchemaObject : remove all the references to other objects,
896     * and all the Maps.
897     */
898    public void clear()
899    {
900        // Clear the extensions
901        for ( String extension : extensions.keySet() )
902        {
903            List<String> extensionList = extensions.get( extension );
904
905            extensionList.clear();
906        }
907
908        extensions.clear();
909
910        // Clear the names
911        names.clear();
912    }
913
914
915    public void unlock()
916    {
917        locked = false;
918    }
919
920
921    /**
922     * {@inheritDoc}
923     */
924    public final void lock()
925    {
926        if ( locked )
927        {
928            return;
929        }
930
931        h = 37;
932
933        // The OID
934        h += h * 17 + oid.hashCode();
935
936        // The SchemaObject type
937        h += h * 17 + objectType.getValue();
938
939        // The Names, if any
940        if ( ( names != null ) && ( names.size() != 0 ) )
941        {
942            for ( String name : names )
943            {
944                h += h * 17 + name.hashCode();
945            }
946        }
947
948        // The schemaName if any
949        if ( schemaName != null )
950        {
951            h += h * 17 + schemaName.hashCode();
952        }
953
954        h += h * 17 + ( isEnabled ? 1 : 0 );
955        h += h * 17 + ( isReadOnly ? 1 : 0 );
956
957        // The description, if any
958        if ( description != null )
959        {
960            h += h * 17 + description.hashCode();
961        }
962
963        // The extensions, if any
964        for ( String key : extensions.keySet() )
965        {
966            h += h * 17 + key.hashCode();
967
968            List<String> values = extensions.get( key );
969
970            if ( values != null )
971            {
972                for ( String value : values )
973                {
974                    h += h * 17 + value.hashCode();
975                }
976            }
977        }
978
979        locked = true;
980    }
981}