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