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 */
019package org.apache.directory.api.ldap.model.entry;
020
021
022import java.io.IOException;
023import java.io.ObjectInput;
024import java.io.ObjectOutput;
025import java.util.Comparator;
026
027import org.apache.directory.api.i18n.I18n;
028import org.apache.directory.api.ldap.model.exception.LdapException;
029import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
030import org.apache.directory.api.ldap.model.schema.AttributeType;
031import org.apache.directory.api.ldap.model.schema.MatchingRule;
032import org.apache.directory.api.ldap.model.schema.Normalizer;
033import org.apache.directory.api.util.Serialize;
034import org.apache.directory.api.util.Strings;
035import org.apache.directory.api.util.exception.NotImplementedException;
036
037
038/**
039 * A server side schema aware wrapper around a String attribute value.
040 * This value wrapper uses schema information to syntax check values,
041 * and to compare them for equality and ordering.  It caches results
042 * and invalidates them when the wrapped value changes.
043 *
044 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
045 */
046public class StringValue extends AbstractValue<String>
047{
048    /** Used for serialization */
049    private static final long serialVersionUID = 2L;
050
051    /** The UTF-8 bytes for this value */
052    private byte[] bytes;
053
054
055    // -----------------------------------------------------------------------
056    // Constructors
057    // -----------------------------------------------------------------------
058    /**
059     * Creates a StringValue without an initial wrapped value.
060     *
061     * @param attributeType the schema attribute type associated with this StringValue
062     */
063    public StringValue( AttributeType attributeType )
064    {
065        if ( attributeType != null )
066        {
067            // We must have a Syntax
068            if ( attributeType.getSyntax() == null )
069            {
070                throw new IllegalArgumentException( I18n.err( I18n.ERR_04445 ) );
071            }
072
073            if ( !attributeType.getSyntax().isHumanReadable() )
074            {
075                LOG.warn( "Treating a value of a binary attribute {} as a String: "
076                    + "\nthis could cause data corruption!", attributeType.getName() );
077            }
078
079            this.attributeType = attributeType;
080        }
081    }
082
083
084    /**
085     * Creates a StringValue with an initial wrapped String value.
086     *
087     * @param value the value to wrap which can be null
088     */
089    public StringValue( String value )
090    {
091        this.wrappedValue = value;
092        this.normalizedValue = value;
093        bytes = Strings.getBytesUtf8( value );
094    }
095
096
097    /**
098     * Creates a schema aware StringValue with an initial wrapped String value.
099     *
100     * @param attributeType the schema type associated with this StringValue
101     * @param value the value to wrap
102     * @throws LdapInvalidAttributeValueException If the added value is invalid accordingly
103     * to the schema
104     */
105    public StringValue( AttributeType attributeType, String value ) throws LdapInvalidAttributeValueException
106    {
107        this( value );
108        apply( attributeType );
109    }
110
111
112    // -----------------------------------------------------------------------
113    // Value<String> Methods
114    // -----------------------------------------------------------------------
115    /**
116     * {@inheritDoc}
117     */
118    public String getValue()
119    {
120        // The String is immutable, we can safely return the internal
121        // object without copying it.
122        return wrappedValue;
123    }
124
125
126    /**
127     * {@inheritDoc}
128     */
129    public String getNormValue()
130    {
131        return normalizedValue;
132    }
133
134
135    // -----------------------------------------------------------------------
136    // Comparable<String> Methods
137    // -----------------------------------------------------------------------
138    /**
139     * @see ServerValue#compareTo(Value)
140     * @throws IllegalStateException on failures to extract the comparator, or the
141     * normalizers needed to perform the required comparisons based on the schema
142     */
143    public int compareTo( Value<String> value )
144    {
145        if ( isNull() )
146        {
147            if ( ( value == null ) || value.isNull() )
148            {
149                return 0;
150            }
151            else
152            {
153                return -1;
154            }
155        }
156        else if ( ( value == null ) || value.isNull() )
157        {
158            return 1;
159        }
160
161        if ( !( value instanceof StringValue ) )
162        {
163            String message = I18n.err( I18n.ERR_04128, toString(), value.getClass() );
164            LOG.error( message );
165            throw new NotImplementedException( message );
166        }
167
168        StringValue stringValue = ( StringValue ) value;
169
170        if ( attributeType != null )
171        {
172            if ( stringValue.getAttributeType() == null )
173            {
174                return getNormValue().compareTo( stringValue.getNormValue() );
175            }
176            else
177            {
178                if ( !attributeType.equals( stringValue.getAttributeType() ) )
179                {
180                    String message = I18n.err( I18n.ERR_04128, toString(), value.getClass() );
181                    LOG.error( message );
182                    throw new NotImplementedException( message );
183                }
184            }
185        }
186        else
187        {
188            return getNormValue().compareTo( stringValue.getNormValue() );
189        }
190
191        try
192        {
193            return getLdapComparator().compare( getNormValue(), stringValue.getNormValue() );
194        }
195        catch ( LdapException e )
196        {
197            String msg = I18n.err( I18n.ERR_04443, this, value );
198            LOG.error( msg, e );
199            throw new IllegalStateException( msg, e );
200        }
201    }
202
203
204    // -----------------------------------------------------------------------
205    // Cloneable methods
206    // -----------------------------------------------------------------------
207    /**
208     * {@inheritDoc}
209     */
210    public StringValue clone()
211    {
212        return ( StringValue ) super.clone();
213    }
214
215
216    // -----------------------------------------------------------------------
217    // Object Methods
218    // -----------------------------------------------------------------------
219    /**
220     * @see Object#hashCode()
221     * @return the instance's hashcode
222     */
223    public int hashCode()
224    {
225        if ( h == 0 )
226        {
227            // return zero if the value is null so only one null value can be
228            // stored in an attribute - the binary version does the same
229            if ( isNull() )
230            {
231                return 0;
232            }
233
234            // If the normalized value is null, will default to wrapped
235            // which cannot be null at this point.
236            // If the normalized value is null, will default to wrapped
237            // which cannot be null at this point.
238            String normalized = getNormValue();
239
240            if ( normalized != null )
241            {
242                h = normalized.hashCode();
243            }
244            else
245            {
246                h = 17;
247            }
248        }
249
250        return h;
251    }
252
253
254    /**
255     * Two StringValue are equals if their normalized values are equal
256     * 
257     * @see Object#equals(Object)
258     */
259    public boolean equals( Object obj )
260    {
261        if ( this == obj )
262        {
263            return true;
264        }
265
266        if ( !( obj instanceof StringValue ) )
267        {
268            return false;
269        }
270
271        StringValue other = ( StringValue ) obj;
272
273        // First check if we have an attrbuteType.
274        if ( attributeType != null )
275        {
276            // yes : check for the other value
277            if ( other.attributeType != null )
278            {
279                if ( attributeType.getOid().equals( other.getAttributeType().getOid() ) )
280                {
281                    // Both AttributeType have the same OID, we can assume they are 
282                    // equals. We don't check any further, because the unicity of OID
283                    // makes it unlikely that the two AT are different.
284                    // The values may be both null
285                    if ( isNull() )
286                    {
287                        return other.isNull();
288                    }
289
290                    // Shortcut : if we have an AT for both the values, check the 
291                    // already normalized values
292                    if ( wrappedValue.equals( other.wrappedValue ) )
293                    {
294                        return true;
295                    }
296
297                    // We have an AttributeType, we use the associated comparator
298                    try
299                    {
300                        Comparator<String> comparator = getLdapComparator();
301
302                        // Compare normalized values
303                        if ( comparator == null )
304                        {
305                            return getNormReference().equals( other.getNormReference() );
306                        }
307                        else
308                        {
309                            return comparator.compare( getNormReference(), other.getNormReference() ) == 0;
310                        }
311                    }
312                    catch ( LdapException ne )
313                    {
314                        return false;
315                    }
316                }
317                else
318                {
319                    // We can't compare two values when the two ATs are different
320                    return false;
321                }
322            }
323            else
324            {
325                // We only have one AT : we will assume that both values are for the 
326                // same AT.
327                // The values may be both null
328                if ( isNull() )
329                {
330                    return other.isNull();
331                }
332
333                // We have an AttributeType on the base value, we need to use its comparator
334                try
335                {
336                    Comparator<String> comparator = getLdapComparator();
337
338                    // Compare normalized values. We have to normalized the other value,
339                    // as it has no AT
340                    MatchingRule equality = getAttributeType().getEquality();
341
342                    if ( equality == null )
343                    {
344                        // No matching rule : compare the raw values
345                        return getNormReference().equals( other.getNormReference() );
346                    }
347
348                    Normalizer normalizer = equality.getNormalizer();
349
350                    StringValue otherValue = ( StringValue ) normalizer.normalize( other );
351
352                    if ( comparator == null )
353                    {
354                        return getNormReference().equals( otherValue.getNormReference() );
355                    }
356                    else
357                    {
358                        return comparator.compare( getNormReference(), otherValue.getNormReference() ) == 0;
359                    }
360                }
361                catch ( LdapException ne )
362                {
363                    return false;
364                }
365            }
366        }
367        else
368        {
369            // No : check for the other value
370            if ( other.attributeType != null )
371            {
372                // We only have one AT : we will assume that both values are for the 
373                // same AT.
374                // The values may be both null
375                if ( isNull() )
376                {
377                    return other.isNull();
378                }
379
380                try
381                {
382                    Comparator<String> comparator = other.getLdapComparator();
383
384                    // Compare normalized values. We have to normalized the other value,
385                    // as it has no AT
386                    MatchingRule equality = other.getAttributeType().getEquality();
387
388                    if ( equality == null )
389                    {
390                        // No matching rule : compare the raw values
391                        return getNormReference().equals( other.getNormReference() );
392                    }
393
394                    Normalizer normalizer = equality.getNormalizer();
395
396                    StringValue thisValue = ( StringValue ) normalizer.normalize( this );
397
398                    if ( comparator == null )
399                    {
400                        return thisValue.getNormReference().equals( other.getNormReference() );
401                    }
402                    else
403                    {
404                        return comparator.compare( thisValue.getNormReference(), other.getNormReference() ) == 0;
405                    }
406                }
407                catch ( LdapException ne )
408                {
409                    return false;
410                }
411            }
412            else
413            {
414                // The values may be both null
415                if ( isNull() )
416                {
417                    return other.isNull();
418                }
419
420                // Now check the normalized values
421                return getNormReference().equals( other.getNormReference() );
422            }
423        }
424    }
425
426
427    /**
428     * {@inheritDoc}
429     */
430    public boolean isHumanReadable()
431    {
432        return true;
433    }
434
435
436    /**
437     * @return The length of the interned value
438     */
439    public int length()
440    {
441        return wrappedValue != null ? wrappedValue.length() : 0;
442    }
443
444
445    /**
446     * Get the wrapped value as a byte[].
447     * @return the wrapped value as a byte[]
448     */
449    public byte[] getBytes()
450    {
451        return bytes;
452    }
453
454
455    /**
456     * Get the wrapped value as a String.
457     *
458     * @return the wrapped value as a String
459     */
460    public String getString()
461    {
462        return wrappedValue != null ? wrappedValue : "";
463    }
464
465
466    /**
467     * Deserialize a StringValue. It will return a new StringValue instance.
468     * 
469     * @param in The input stream
470     * @return A new StringValue instance
471     * @throws IOException If the stream can't be read
472     * @throws ClassNotFoundException If we can't instanciate a StringValue
473     */
474    public static StringValue deserialize( ObjectInput in ) throws IOException, ClassNotFoundException
475    {
476        StringValue value = new StringValue( ( AttributeType ) null );
477        value.readExternal( in );
478
479        return value;
480    }
481
482
483    /**
484     * Deserialize a schemaAware StringValue. It will return a new StringValue instance.
485     * 
486     * @param attributeType The AttributeType associated with the Value. Can be null
487     * @param in The input stream
488     * @return A new StringValue instance
489     * @throws IOException If the stream can't be read
490     * @throws ClassNotFoundException If we can't instanciate a StringValue
491     */
492    public static StringValue deserialize( AttributeType attributeType, ObjectInput in ) throws IOException,
493        ClassNotFoundException
494    {
495        StringValue value = new StringValue( attributeType );
496        value.readExternal( in );
497
498        return value;
499    }
500
501
502    /**
503     * {@inheritDoc}
504     */
505    public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
506    {
507        // Read the STRING flag
508        boolean isHR = in.readBoolean();
509
510        if ( !isHR )
511        {
512            throw new IOException( "The serialized value is not a String value" );
513        }
514
515        // Read the wrapped value, if it's not null
516        if ( in.readBoolean() )
517        {
518            wrappedValue = in.readUTF();
519            bytes = Strings.getBytesUtf8( wrappedValue );
520        }
521
522        // Read the isNormalized flag
523        boolean normalized = in.readBoolean();
524
525        if ( normalized )
526        {
527            // Read the normalized value, if not null
528            if ( in.readBoolean() )
529            {
530                normalizedValue = in.readUTF();
531            }
532        }
533        else
534        {
535            if ( attributeType != null )
536            {
537                try
538                {
539                    MatchingRule equality = attributeType.getEquality();
540
541                    if ( equality == null )
542                    {
543                        normalizedValue = wrappedValue;
544                    }
545                    else
546                    {
547                        Normalizer normalizer = equality.getNormalizer();
548
549                        if ( normalizer != null )
550                        {
551                            normalizedValue = normalizer.normalize( wrappedValue );
552                        }
553                        else
554                        {
555                            normalizedValue = wrappedValue;
556                        }
557                    }
558                }
559                catch ( LdapException le )
560                {
561                    normalizedValue = wrappedValue;
562                }
563            }
564            else
565            {
566                normalizedValue = wrappedValue;
567            }
568        }
569
570        // The hashCoe
571        h = in.readInt();
572    }
573
574
575    /**
576     * Serialize the StringValue into a buffer at the given position.
577     * 
578     * @param buffer The buffer which will contain the serialized StringValue
579     * @param pos The position in the buffer for the serialized value
580     * @return The new position in the buffer
581     */
582    public int serialize( byte[] buffer, int pos )
583    {
584        // Compute the length
585        int length = 1 + 1 + 1 + 4; // The value type, the wrappedValue presence flag,
586                                    // the normalizedValue presence flag and the hash length.
587
588        byte[] wrappedValueBytes = null;
589        byte[] normalizedValueBytes = null;
590
591        if ( wrappedValue != null )
592        {
593            wrappedValueBytes = Strings.getBytesUtf8( wrappedValue );
594            length += 4 + wrappedValueBytes.length;
595        }
596
597        if ( attributeType != null )
598        {
599            if ( normalizedValue != null )
600            {
601                normalizedValueBytes = Strings.getBytesUtf8( normalizedValue );
602                length += 1 + 4 + normalizedValueBytes.length;
603            }
604            else
605            {
606                length += 1;
607            }
608        }
609
610        // Check that we will be able to store the data in the buffer
611        if ( buffer.length - pos < length )
612        {
613            throw new ArrayIndexOutOfBoundsException();
614        }
615
616        // The STRING flag
617        buffer[pos] = Serialize.TRUE;
618        pos++;
619
620        // Write the wrapped value, if it's not null
621        if ( wrappedValue != null )
622        {
623            buffer[pos++] = Serialize.TRUE;
624            pos = Serialize.serialize( wrappedValueBytes, buffer, pos );
625        }
626        else
627        {
628            buffer[pos++] = Serialize.FALSE;
629        }
630
631        // Write the isNormalized flag
632        if ( attributeType != null )
633        {
634            // This flag is present to tell that we have a normalized value different
635            // from the upValue
636
637            buffer[pos++] = Serialize.TRUE;
638
639            // Write the normalized value, if not null
640            if ( normalizedValue != null )
641            {
642                buffer[pos++] = Serialize.TRUE;
643                pos = Serialize.serialize( normalizedValueBytes, buffer, pos );
644            }
645            else
646            {
647                buffer[pos++] = Serialize.FALSE;
648            }
649        }
650        else
651        {
652            // No normalized value
653            buffer[pos++] = Serialize.FALSE;
654        }
655
656        // Write the hashCode
657        pos = Serialize.serialize( h, buffer, pos );
658
659        return pos;
660    }
661
662
663    /**
664     * Deserialize a StringValue from a byte[], starting at a given position
665     * 
666     * @param buffer The buffer containing the StringValue
667     * @param pos The position in the buffer
668     * @return The new position
669     * @throws IOException If the serialized value is not a StringValue
670     */
671    public int deserialize( byte[] buffer, int pos ) throws IOException
672    {
673        if ( ( pos < 0 ) || ( pos >= buffer.length ) )
674        {
675            throw new ArrayIndexOutOfBoundsException();
676        }
677
678        // Read the STRING flag
679        boolean isHR = Serialize.deserializeBoolean( buffer, pos );
680        pos++;
681
682        if ( !isHR )
683        {
684            throw new IOException( "The serialized value is not a String value" );
685        }
686
687        // Read the wrapped value, if it's not null
688        boolean hasWrappedValue = Serialize.deserializeBoolean( buffer, pos );
689        pos++;
690
691        if ( hasWrappedValue )
692        {
693            byte[] wrappedValueBytes = Serialize.deserializeBytes( buffer, pos );
694            pos += 4 + wrappedValueBytes.length;
695            wrappedValue = Strings.utf8ToString( wrappedValueBytes );
696        }
697
698        // Read the isNormalized flag
699        boolean hasAttributeType = Serialize.deserializeBoolean( buffer, pos );
700        pos++;
701
702        if ( hasAttributeType )
703        {
704            // Read the normalized value, if not null
705            boolean hasNormalizedValue = Serialize.deserializeBoolean( buffer, pos );
706            pos++;
707
708            if ( hasNormalizedValue )
709            {
710                byte[] normalizedValueBytes = Serialize.deserializeBytes( buffer, pos );
711                pos += 4 + normalizedValueBytes.length;
712                normalizedValue = Strings.utf8ToString( normalizedValueBytes );
713            }
714        }
715        else
716        {
717            if ( attributeType != null )
718            {
719                try
720                {
721                    MatchingRule equality = attributeType.getEquality();
722
723                    if ( equality == null )
724                    {
725                        normalizedValue = wrappedValue;
726                    }
727                    else
728                    {
729                        Normalizer normalizer = equality.getNormalizer();
730
731                        if ( normalizer != null )
732                        {
733                            normalizedValue = normalizer.normalize( wrappedValue );
734                        }
735                        else
736                        {
737                            normalizedValue = wrappedValue;
738                        }
739                    }
740                }
741                catch ( LdapException le )
742                {
743                    normalizedValue = wrappedValue;
744                }
745            }
746            else
747            {
748                normalizedValue = wrappedValue;
749            }
750        }
751
752        // The hashCode
753        h = Serialize.deserializeInt( buffer, pos );
754        pos += 4;
755
756        return pos;
757    }
758
759
760    /**
761     * {@inheritDoc}
762     */
763    public void writeExternal( ObjectOutput out ) throws IOException
764    {
765        // Write a boolean for the HR flag
766        out.writeBoolean( STRING );
767
768        // Write the wrapped value, if it's not null
769        if ( wrappedValue != null )
770        {
771            out.writeBoolean( true );
772            out.writeUTF( wrappedValue );
773        }
774        else
775        {
776            out.writeBoolean( false );
777        }
778
779        // Write the isNormalized flag
780        if ( attributeType != null )
781        {
782            // This flag is present to tell that we have a normalized value different
783            // from the upValue
784            out.writeBoolean( true );
785
786            // Write the normalized value, if not null
787            if ( normalizedValue != null )
788            {
789                out.writeBoolean( true );
790                out.writeUTF( normalizedValue );
791            }
792            else
793            {
794                out.writeBoolean( false );
795            }
796        }
797        else
798        {
799            // No normalized value
800            out.writeBoolean( false );
801        }
802
803        // Write the hashCode
804        out.writeInt( h );
805
806        // and flush the data
807        out.flush();
808    }
809
810
811    /**
812     * @see Object#toString()
813     */
814    public String toString()
815    {
816        return wrappedValue == null ? "null" : wrappedValue;
817    }
818}