blob: 1ac00141db213d17ea3e9c2ff3740f624ba82cc6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.directory.shared.ldap.model.entry;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.directory.shared.asn1.util.OID;
import org.apache.directory.shared.i18n.I18n;
import org.apache.directory.shared.ldap.model.exception.LdapException;
import org.apache.directory.shared.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.shared.ldap.model.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.model.schema.AttributeType;
import org.apache.directory.shared.ldap.model.schema.SyntaxChecker;
import org.apache.directory.shared.util.Strings;
import org.apache.directory.shared.util.Unicode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A client side entry attribute. The client is not aware of the schema,
* so we can't tell if the stored value will be String or Binary. We will
* default to Binary.<p>
* To define the kind of data stored, the client must set the isHR flag.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class DefaultEntryAttribute implements EntryAttribute
{
/** logger for reporting errors that might not be handled properly upstream */
private static final Logger LOG = LoggerFactory.getLogger( DefaultEntryAttribute.class );
/** The associated AttributeType */
private AttributeType attributeType;
/** The set of contained values */
private Set<Value<?>> values = new LinkedHashSet<Value<?>>();
/** The User provided ID */
private String upId;
/** The normalized ID (will be the OID if we have a AttributeType) */
private String id;
/** Tells if the attribute is Human Readable or not. When not set,
* this flag is null. */
private Boolean isHR;
/** The computed hashcode. We don't want to compute it each time the hashcode() method is called */
private volatile int h;
//-------------------------------------------------------------------------
// Helper methods
//-------------------------------------------------------------------------
private Value<String> createStringValue( AttributeType attributeType, String value )
{
Value<String> stringValue = null;
if ( attributeType != null )
{
stringValue = new StringValue( attributeType, value );
try
{
stringValue.normalize();
}
catch( LdapException ne )
{
// The value can't be normalized : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, value ) );
return null;
}
if ( !stringValue.isValid() )
{
// The value is not valid : we don't add it.
LOG.error( I18n.err( I18n.ERR_04450, value ) );
return null;
}
}
else
{
stringValue = new StringValue( value );
}
return stringValue;
}
private Value<byte[]> createBinaryValue( AttributeType attributeType, byte[] value )
{
Value<byte[]> binaryValue = null;
if ( attributeType != null )
{
binaryValue = new BinaryValue( attributeType, value );
try
{
binaryValue.normalize();
}
catch( LdapException ne )
{
// The value can't be normalized : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, value ) );
return null;
}
if ( !binaryValue.isValid() )
{
// The value is not valid : we don't add it.
LOG.error( I18n.err( I18n.ERR_04450, value ) );
return null;
}
}
else
{
binaryValue = new BinaryValue( value );
}
return binaryValue;
}
//-------------------------------------------------------------------------
// Constructors
//-------------------------------------------------------------------------
// maybe have some additional convenience constructors which take
// an initial value as a string or a byte[]
/**
* Create a new instance of a EntryAttribute, without ID nor value.
*/
public DefaultEntryAttribute()
{
}
/**
* Create a new instance of a EntryAttribute, without ID nor value.
*/
/* No qualifier */ DefaultEntryAttribute( AttributeType attributeType, String upId, String normId, boolean isHR, int hashCode, Value<?>... values)
{
this.attributeType = attributeType;
this.upId = upId;
this.id = normId;
this.isHR = isHR;
this.h = hashCode;
if ( values != null )
{
for ( Value<?> value : values )
{
this.values.add( value );
}
}
}
/**
* Create a new instance of a EntryAttribute, without ID nor value.
*
* @param attributeType the attributeType for the empty attribute added into the entry
*/
public DefaultEntryAttribute( AttributeType attributeType )
{
if ( attributeType == null )
{
String message = I18n.err( I18n.ERR_04442_NULL_AT_NOT_ALLOWED );
LOG.error( message );
throw new IllegalArgumentException( message );
}
setAttributeType( attributeType );
}
/**
* Create a new instance of a EntryAttribute, without value.
*/
public DefaultEntryAttribute( String upId )
{
setUpId( upId );
}
/**
* Create a new instance of a EntryAttribute, without value.
*
* @param upId the ID for the added attributeType
* @param attributeType the added AttributeType
*/
public DefaultEntryAttribute( String upId, AttributeType attributeType )
{
if ( attributeType == null )
{
String message = I18n.err( I18n.ERR_04442_NULL_AT_NOT_ALLOWED );
LOG.error( message );
throw new IllegalArgumentException( message );
}
setAttributeType( attributeType );
setUpId( upId, attributeType );
}
/**
* If the value does not correspond to the same attributeType, then it's
* wrapped value is copied into a new ClientValue which uses the specified
* attributeType.
*
* Otherwise, the value is stored, but as a reference. It's not a copy.
*
* @param upId
* @param vals an initial set of values for this attribute
*/
public DefaultEntryAttribute( String upId, Value<?>... vals )
{
// The value can be null, this is a valid value.
if ( vals[0] == null )
{
add( new StringValue() );
}
else
{
for ( Value<?> val:vals )
{
if ( ( val instanceof StringValue) || ( val.isBinary() ) )
{
add( val );
}
else
{
String message = I18n.err( I18n.ERR_04129, val.getClass().getName() );
LOG.error( message );
throw new IllegalStateException( message );
}
}
}
setUpId( upId );
}
/**
* Create a new instance of a EntryAttribute, without ID but with some values.
*
* @param attributeType The attributeType added on creation
* @param vals The added value for this attribute
*/
public DefaultEntryAttribute( AttributeType attributeType, String... vals )
{
this( null, attributeType, vals );
}
/**
* Create a new instance of a EntryAttribute.
*
* @param upId the ID for the added attribute
* @param attributeType The attributeType added on creation
* @param vals the added values for this attribute
*/
public DefaultEntryAttribute( String upId, AttributeType attributeType, String... vals )
{
if ( attributeType == null )
{
String message = I18n.err( I18n.ERR_04442_NULL_AT_NOT_ALLOWED );
LOG.error( message );
throw new IllegalArgumentException( message );
}
setAttributeType( attributeType );
add( vals );
setUpId( upId, attributeType );
}
/**
* Doc me more!
*
* If the value does not correspond to the same attributeType, then it's
* wrapped value is copied into a new Value which uses the specified
* attributeType.
*
* Otherwise, the value is stored, but as a reference. It's not a copy.
*
* @param upId the ID of the added attribute
* @param attributeType the attribute type according to the schema
* @param vals an initial set of values for this attribute
*/
public DefaultEntryAttribute( String upId, AttributeType attributeType, Value<?>... vals )
{
if ( attributeType == null )
{
String message = I18n.err( I18n.ERR_04442_NULL_AT_NOT_ALLOWED );
LOG.error( message );
throw new IllegalArgumentException( message );
}
setAttributeType( attributeType );
setUpId( upId, attributeType );
add( vals );
}
/**
* Doc me more!
*
* If the value does not correspond to the same attributeType, then it's
* wrapped value is copied into a new Value which uses the specified
* attributeType.
*
* @param attributeType the attribute type according to the schema
* @param vals an initial set of values for this attribute
*/
public DefaultEntryAttribute( AttributeType attributeType, Value<?>... vals )
{
this( null, attributeType, vals );
}
/**
* Create a new instance of a EntryAttribute.
*/
public DefaultEntryAttribute( String upId, String... vals )
{
add( vals );
setUpId( upId );
}
/**
* Create a new instance of a EntryAttribute, with some byte[] values.
*/
public DefaultEntryAttribute( String upId, byte[]... vals )
{
add( vals );
setUpId( upId );
}
/**
* Create a new instance of a EntryAttribute, with some byte[] values.
*
* @param attributeType The attributeType added on creation
* @param vals The value for the added attribute
*/
public DefaultEntryAttribute( AttributeType attributeType, byte[]... vals )
{
this( null, attributeType, vals );
}
/**
* Create a new instance of a EntryAttribute, with some byte[] values.
*
* @param upId the ID for the added attribute
* @param attributeType the AttributeType to be added
* @param vals the values for the added attribute
*/
public DefaultEntryAttribute( String upId, AttributeType attributeType, byte[]... vals )
{
if ( attributeType == null )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_04442_NULL_AT_NOT_ALLOWED ) );
}
setAttributeType( attributeType );
add( vals );
setUpId( upId, attributeType );
}
/**
*
* Creates a new instance of DefaultServerAttribute, by copying
* another attribute, which can be a ClientAttribute. If the other
* attribute is a ServerAttribute, it will be copied.
*
* @param attributeType The attribute's type
* @param attribute The attribute to be copied
*/
public DefaultEntryAttribute( AttributeType attributeType, EntryAttribute attribute )
{
// Copy the common values. isHR is only available on a ServerAttribute
this.attributeType = attributeType;
this.id = attribute.getId();
this.upId = attribute.getUpId();
if ( attributeType == null )
{
isHR = attribute.isHR();
// Copy all the values
for ( Value<?> value:attribute )
{
add( value.clone() );
}
}
else
{
isHR = attributeType.getSyntax().isHumanReadable();
// Copy all the values
for ( Value<?> clientValue:attribute )
{
Value<?> serverValue = null;
// We have to convert the value first
if ( clientValue instanceof StringValue)
{
if ( isHR )
{
serverValue = new StringValue( attributeType, clientValue.getString() );
}
else
{
// We have to convert the value to a binary value first
serverValue = new BinaryValue( attributeType,
clientValue.getBytes() );
}
}
else if ( clientValue instanceof BinaryValue )
{
if ( isHR )
{
// We have to convert the value to a String value first
serverValue = new StringValue( attributeType,
clientValue.getString() );
}
else
{
serverValue = new BinaryValue( attributeType, clientValue.getBytes() );
}
}
add( serverValue );
}
}
}
/**
* <p>
* Get the byte[] value, if and only if the value is known to be Binary,
* otherwise a InvalidAttributeValueException will be thrown
* </p>
* <p>
* Note that this method returns the first value only.
* </p>
*
* @return The value as a byte[]
* @throws LdapInvalidAttributeValueException If the value is a String
*/
public byte[] getBytes() throws LdapInvalidAttributeValueException
{
Value<?> value = get();
if ( value.isBinary() )
{
return value.getBytes();
}
else
{
String message = I18n.err( I18n.ERR_04130 );
LOG.error( message );
throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, message );
}
}
/**
* <p>
* Get the String value, if and only if the value is known to be a String,
* otherwise a InvalidAttributeValueException will be thrown
* </p>
* <p>
* Note that this method returns the first value only.
* </p>
*
* @return The value as a String
* @throws LdapInvalidAttributeValueException If the value is a byte[]
*/
public String getString() throws LdapInvalidAttributeValueException
{
Value<?> value = get();
if ( value instanceof StringValue)
{
return value.getString();
}
else
{
if ( isHR )
{
// Try to convert the value from a byte[] to a String
if ( value != null )
{
String valueStr = Strings.utf8ToString((byte[]) value.getReference());
return valueStr;
}
}
String message = I18n.err( I18n.ERR_04131 );
LOG.error( message );
throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, message );
}
}
/**
* Get's the attribute identifier. Its value is the same than the
* user provided ID.
*
* @return the attribute's identifier
*/
public String getId()
{
return id;
}
/**
* <p>
* Set the attribute to Human Readable or to Binary.
* </p>
* @param isHR <code>true</code> for a Human Readable attribute,
* <code>false</code> for a Binary attribute.
*/
public void setHR( boolean isHR )
{
//TODO : deal with the values, we may have to convert them.
if ( attributeType == null )
{
this.isHR = isHR;
// Compute the hashCode
rehash();
}
}
/**
* Set the Attribute ID.
*
* @param id The attribute ID
* @throws IllegalArgumentException If the ID is empty or null or
* resolve to an empty value after being trimmed
*/
public void setId( String id )
{
String newId = Strings.trim(Strings.lowerCaseAscii(id));
if ( newId.length() == 0 )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_04132 ) );
}
if ( attributeType != null )
{
if ( attributeType.getName() == null )
{
// If the name is null, then we may have to store an OID
if ( !OID.isOID( newId ) || !attributeType.getOid().equals( newId ) )
{
// This is an error
throw new IllegalArgumentException( I18n.err( I18n.ERR_04132 ) );
}
}
else
{
// We have at least one name. Check that the normalized upId
// is one of those names. Otherwise, the upId may be an OID too.
// In this case, it must be equals to the attributeType OID.
for ( String atName:attributeType.getNames() )
{
if ( atName.equalsIgnoreCase( newId ) )
{
// Found ! We can store the upId and get out
this.id = newId;
this.upId = id;
// Compute the hashCode
rehash();
return;
}
}
// Last case, the UpId is an OID
if ( !OID.isOID(newId) || !attributeType.getOid().equals( newId ) )
{
// The id is incorrect : this is not allowed
throw new IllegalArgumentException( I18n.err( I18n.ERR_04455, id, attributeType.getName() ) );
}
}
}
this.id = newId;
this.upId = id;
// Compute the hashCode
rehash();
}
/**
* Get's the user provided identifier for this entry. This is the value
* that will be used as the identifier for the attribute within the
* entry. If this is a commonName attribute for example and the user
* provides "COMMONname" instead when adding the entry then this is
* the format the user will have that entry returned by the directory
* server. To do so we store this value as it was given and track it
* in the attribute using this property.
*
* @return the user provided identifier for this attribute
*/
public String getUpId()
{
return upId;
}
/**
* Set the user provided ID. It will also set the ID, normalizing
* the upId (removing spaces before and after, and lowercasing it)<br>
* <br>
* If the Attribute already has an AttributeType, then the upId must
* be either the AttributeType name, or OID
*
* @param upId The attribute ID
* @throws IllegalArgumentException If the ID is empty or null or
* resolve to an empty value after being trimmed
*/
public void setUpId( String upId )
{
setUpId( upId, null );
}
/**
* Check that the upId is either a name or the OID of a given AT
*/
private boolean areCompatible( String id, AttributeType attributeType )
{
// First, get rid of the options, if any
int optPos = id.indexOf( ";" );
String idNoOption = id;
if ( optPos != -1 )
{
idNoOption = id.substring( 0, optPos );
}
// Check that we find the ID in the AT names
for ( String name : attributeType.getNames() )
{
if ( name.equalsIgnoreCase( idNoOption ) )
{
return true;
}
}
// Not found in names, check the OID
return OID.isOID(id) && attributeType.getOid().equals(id);
}
/**
* <p>
* Set the user provided ID. If we have none, the upId is assigned
* the attributetype's name. If it does not have any name, we will
* use the OID.
* </p>
* <p>
* If we have an upId and an AttributeType, they must be compatible. :
* - if the upId is an OID, it must be the AttributeType's OID
* - otherwise, its normalized form must be equals to ones of
* the attributeType's names.
* </p>
* <p>
* In any case, the ATtributeType will be changed. The caller is responsible for
* the present values to be compatoble with the new AttributeType.
* </p>
*
* @param upId The attribute ID
* @param attributeType The associated attributeType
*/
public void setUpId( String upId, AttributeType attributeType )
{
String trimmed = Strings.trim(upId);
if ( Strings.isEmpty(trimmed) && ( attributeType == null ) )
{
throw new IllegalArgumentException( "Cannot set a null ID with a null AttributeType" );
}
String id = Strings.toLowerCase(trimmed);
if ( attributeType == null )
{
if ( this.attributeType == null )
{
this.upId = upId;
this.id = id;
// Compute the hashCode
rehash();
return;
}
else
{
if ( areCompatible( id, this.attributeType ) )
{
this.upId = upId;
this.id = id;
// Compute the hashCode
rehash();
return;
}
else
{
return;
}
}
}
if ( Strings.isEmpty(id) )
{
this.attributeType = attributeType;
this.upId = attributeType.getName();
this.id = Strings.trim(this.upId);
// Compute the hashCode
rehash();
return;
}
if ( areCompatible( id, attributeType ) )
{
this.upId = upId;
this.id = id;
this.attributeType = attributeType;
// Compute the hashCode
rehash();
return;
}
throw new IllegalArgumentException( "ID '" + id + "' and AttributeType '" + attributeType.getName() + "' are not compatible " );
}
/**
* <p>
* Tells if the attribute is Human Readable.
* </p>
* <p>This flag is set by the caller, or implicitly when adding String
* values into an attribute which is not yet declared as Binary.
* </p>
* @return
*/
public boolean isHR()
{
return isHR != null ? isHR : false;
}
/**
* Checks to see if this attribute is valid along with the values it contains.
*
* @return true if the attribute and it's values are valid, false otherwise
* @throws LdapException if there is a failure to check syntaxes of values
*/
public boolean isValid() throws LdapException
{
if ( attributeType != null )
{
// First check if the attribute has more than one value
// if the attribute is supposed to be SINGLE_VALUE
if ( attributeType.isSingleValued() && ( values.size() > 1 ) )
{
return false;
}
// Check that we can have no value for this attributeType
if ( values.size() == 0 )
{
return attributeType.getSyntax().getSyntaxChecker().isValidSyntax( null );
}
}
for ( Value<?> value:values )
{
if ( !value.isValid() )
{
return false;
}
}
return true;
}
/**
* Checks to see if this attribute is valid along with the values it contains.
*
* @return true if the attribute and it's values are valid, false otherwise
* @throws LdapException if there is a failure to check syntaxes of values
*/
public boolean isValid( SyntaxChecker checker ) throws LdapException
{
for ( Value<?> value : values )
{
if ( !value.isValid( checker ) )
{
return false;
}
}
return true;
}
/**
* Adds some values to this attribute. If the new values are already present in
* the attribute values, the method has no effect.
* <p>
* The new values are added at the end of list of values.
* </p>
* <p>
* This method returns the number of values that were added.
* </p>
* <p>
* If the value's type is different from the attribute's type,
* a conversion is done. For instance, if we try to set some
* StringValue into a Binary attribute, we just store the UTF-8
* byte array encoding for this StringValue.
* </p>
* <p>
* If we try to store some BinaryValue in a HR attribute, we try to
* convert those BinaryValue assuming they represent an UTF-8 encoded
* String. Of course, if it's not the case, the stored value will
* be incorrect.
* </p>
* <p>
* It's the responsibility of the caller to check if the stored
* values are consistent with the attribute's type.
* </p>
* <p>
* The caller can set the HR flag in order to enforce a type for
* the current attribute, otherwise this type will be set while
* adding the first value, using the value's type to set the flag.
* </p>
* <p>
* <b>Note : </b>If the entry contains no value, and the unique added value
* is a null length value, then this value will be considered as
* a binary value.
* </p>
* @param vals some new values to be added which may be null
* @return the number of added values, or 0 if none has been added
*/
@edu.umd.cs.findbugs.annotations.SuppressWarnings( value="NP_LOAD_OF_KNOWN_NULL_VALUE",
justification="Validity of null depends on the checker")
public int add( Value<?>... vals )
{
int nbAdded = 0;
BinaryValue nullBinaryValue = null;
StringValue nullStringValue = null;
boolean nullValueAdded = false;
if ( attributeType != null )
{
for ( Value<?> val:vals )
{
if ( attributeType.getSyntax().isHumanReadable() )
{
if ( ( val == null ) || val.isNull() )
{
Value<String> nullSV = new StringValue( attributeType, (String)null );
if ( values.add( nullSV ) )
{
nbAdded++;
}
}
else if ( val instanceof StringValue)
{
StringValue stringValue = (StringValue)val;
if ( stringValue.getAttributeType() == null )
{
stringValue.apply( attributeType );
}
if ( values.add( val ) )
{
nbAdded++;
}
}
else
{
String message = I18n.err( I18n.ERR_04451 );
LOG.error( message );
}
}
else
{
if ( val == null )
{
if ( attributeType.getSyntax().getSyntaxChecker().isValidSyntax( val ) )
{
Value<byte[]> nullSV = new BinaryValue( attributeType, (byte[])null );
if ( values.add( nullSV ) )
{
nbAdded++;
}
}
else
{
String message = I18n.err( I18n.ERR_04452 );
LOG.error( message );
}
}
else
{
if ( val instanceof BinaryValue )
{
BinaryValue binaryValue = (BinaryValue)val;
if ( binaryValue.getAttributeType() == null )
{
binaryValue = new BinaryValue( attributeType, val.getBytes() );
}
if ( values.add( binaryValue ) )
{
nbAdded++;
}
}
else
{
String message = I18n.err( I18n.ERR_04452 );
LOG.error( message );
}
}
}
}
}
else
{
for ( Value<?> val:vals )
{
if ( val == null )
{
// We have a null value. If the HR flag is not set, we will consider
// that the attribute is not HR. We may change this later
if ( isHR == null )
{
// This is the first value. Add both types, as we
// don't know yet the attribute type's, but we may
// know later if we add some new value.
// We have to do that because we are using a Set,
// and we can't remove the first element of the Set.
nullBinaryValue = new BinaryValue( (byte[])null );
nullStringValue = new StringValue( (String)null );
values.add( nullBinaryValue );
values.add( nullStringValue );
nullValueAdded = true;
nbAdded++;
}
else if ( !isHR )
{
// The attribute type is binary.
nullBinaryValue = new BinaryValue( (byte[])null );
// Don't add a value if it already exists.
if ( !values.contains( nullBinaryValue ) )
{
values.add( nullBinaryValue );
nbAdded++;
}
}
else
{
// The attribute is HR
nullStringValue = new StringValue( (String)null );
// Don't add a value if it already exists.
if ( !values.contains( nullStringValue ) )
{
values.add( nullStringValue );
}
}
}
else
{
// Let's check the value type.
if ( val instanceof StringValue)
{
// We have a String value
if ( isHR == null )
{
// The attribute type will be set to HR
isHR = true;
values.add( val );
nbAdded++;
}
else if ( !isHR )
{
// The attributeType is binary, convert the
// value to a BinaryValue
BinaryValue bv = new BinaryValue( val.getBytes() );
if ( !contains( bv ) )
{
values.add( bv );
nbAdded++;
}
}
else
{
// The attributeType is HR, simply add the value
if ( !contains( val ) )
{
values.add( val );
nbAdded++;
}
}
}
else
{
// We have a Binary value
if ( isHR == null )
{
// The attribute type will be set to binary
isHR = false;
values.add( val );
nbAdded++;
}
else if ( !isHR )
{
// The attributeType is not HR, simply add the value if it does not already exist
if ( !contains( val ) )
{
values.add( val );
nbAdded++;
}
}
else
{
// The attribute Type is HR, convert the
// value to a StringValue
StringValue sv = new StringValue( val.getString() );
if ( !contains( sv ) )
{
values.add( sv );
nbAdded++;
}
}
}
}
}
}
// Last, not least, if a nullValue has been added, and if other
// values are all String, we have to keep the correct nullValue,
// and to remove the other
if ( nullValueAdded )
{
if ( isHR )
{
// Remove the Binary value
values.remove( nullBinaryValue );
}
else
{
// Remove the String value
values.remove( nullStringValue );
}
}
return nbAdded;
}
/**
* @see EntryAttribute#add(String...)
*/
public int add( String... vals )
{
int nbAdded = 0;
// First, if the isHR flag is not set, we assume that the
// attribute is HR, because we are asked to add some strings.
if ( isHR == null )
{
isHR = true;
}
// Check the attribute type.
if ( attributeType == null )
{
if ( isHR )
{
for ( String val:vals )
{
Value<String> value = createStringValue( attributeType, val );
if ( value == null )
{
// The value can't be normalized : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, val ) );
continue;
}
// Call the add(Value) method, if not already present
if ( add( value ) == 1 )
{
nbAdded++;
}
else
{
LOG.error( I18n.err( I18n.ERR_04486_VALUE_ALREADY_EXISTS, val, upId ) );
}
}
}
else
{
// The attribute is binary. Transform the String to byte[]
for ( String val:vals )
{
byte[] valBytes = null;
if ( val != null )
{
valBytes = Strings.getBytesUtf8(val);
}
Value<byte[]> value = createBinaryValue( attributeType, valBytes );
if ( value == null )
{
// The value can't be normalized or is invalid : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, val ) );
continue;
}
// Now call the add(Value) method
if ( add( value ) == 1 )
{
nbAdded++;
}
}
}
}
else
{
if ( isHR )
{
for ( String val:vals )
{
Value<String> value = createStringValue( attributeType, val );
if ( value == null )
{
// The value can't be normalized : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, val ) );
continue;
}
// Call the add(Value) method, if not already present
if ( add( value ) == 1 )
{
nbAdded++;
}
else
{
LOG.error( I18n.err( I18n.ERR_04486_VALUE_ALREADY_EXISTS, val, upId ) );
}
}
}
else
{
// The attribute is binary. Transform the String to byte[]
for ( String val:vals )
{
byte[] valBytes = null;
if ( val != null )
{
valBytes = Strings.getBytesUtf8(val);
}
Value<byte[]> value = createBinaryValue( attributeType, valBytes );
if ( value == null )
{
// The value can't be normalized or is invalid : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, val ) );
continue;
}
// Now call the add(Value) method
if ( add( value ) == 1 )
{
nbAdded++;
}
}
}
}
return nbAdded;
}
/**
* Adds some values to this attribute. If the new values are already present in
* the attribute values, the method has no effect.
* <p>
* The new values are added at the end of list of values.
* </p>
* <p>
* This method returns the number of values that were added.
* </p>
* If the value's type is different from the attribute's type,
* a conversion is done. For instance, if we try to set some String
* into a Binary attribute, we just store the UTF-8 byte array
* encoding for this String.
* If we try to store some byte[] in a HR attribute, we try to
* convert those byte[] assuming they represent an UTF-8 encoded
* String. Of course, if it's not the case, the stored value will
* be incorrect.
* <br>
* It's the responsibility of the caller to check if the stored
* values are consistent with the attribute's type.
* <br>
* The caller can set the HR flag in order to enforce a type for
* the current attribute, otherwise this type will be set while
* adding the first value, using the value's type to set the flag.
*
* @param vals some new values to be added which may be null
* @return the number of added values, or 0 if none has been added
*/
public int add( byte[]... vals )
{
int nbAdded = 0;
// First, if the isHR flag is not set, we assume that the
// attribute is not HR, because we are asked to add some byte[].
if ( isHR == null )
{
isHR = false;
}
if ( !isHR )
{
for ( byte[] val:vals )
{
Value<byte[]> value = null;
if ( attributeType == null )
{
value = new BinaryValue( val );
}
else
{
value = new BinaryValue( attributeType, val );
try
{
value.normalize();
}
catch( LdapException ne )
{
// The value can't be normalized : we don't add it.
LOG.error( I18n.err( I18n.ERR_04449, Strings.dumpBytes(val) ) );
return 0;
}
}
if ( add( value ) != 0 )
{
nbAdded++;
}
else
{
LOG.error( I18n.err( I18n.ERR_04486_VALUE_ALREADY_EXISTS, Strings.dumpBytes(val), upId ) );
}
}
}
else
{
// We can't add Binary values into a String Attribute
LOG.info( I18n.err( I18n.ERR_04451 ) );
return 0;
}
return nbAdded;
}
/**
* Remove all the values from this attribute.
*/
public void clear()
{
values.clear();
}
/**
* <p>
* Indicates whether the specified values are some of the attribute's values.
* </p>
* <p>
* If the Attribute is HR, the binary values will be converted to String before
* being checked.
* </p>
*
* @param vals the values
* @return true if this attribute contains all the values, otherwise false
*/
public boolean contains( Value<?>... vals )
{
if ( isHR == null )
{
// If this flag is null, then there is no values.
return false;
}
if ( attributeType == null )
{
if ( isHR )
{
// Iterate through all the values, convert the Binary values
// to String values, and quit id any of the values is not
// contained in the object
for ( Value<?> val:vals )
{
if ( val instanceof StringValue)
{
if ( !values.contains( val ) )
{
return false;
}
}
else
{
byte[] binaryVal = val.getBytes();
// We have to convert the binary value to a String
if ( ! values.contains( new StringValue( Strings.utf8ToString(binaryVal) ) ) )
{
return false;
}
}
}
}
else
{
// Iterate through all the values, convert the String values
// to binary values, and quit id any of the values is not
// contained in the object
for ( Value<?> val:vals )
{
if ( val.isBinary() )
{
if ( !values.contains( val ) )
{
return false;
}
}
else
{
String stringVal = val.getString();
// We have to convert the binary value to a String
if ( ! values.contains( new BinaryValue( Strings.getBytesUtf8(stringVal) ) ) )
{
return false;
}
}
}
}
}
else
{
// Iterate through all the values, and quit if we
// don't find one in the values. We have to separate the check
// depending on the isHR flag value.
if ( isHR )
{
for ( Value<?> val:vals )
{
if ( val instanceof StringValue)
{
StringValue stringValue = (StringValue)val;
if ( stringValue.getAttributeType() == null )
{
stringValue.apply( attributeType );
}
if ( !values.contains( val ) )
{
return false;
}
}
else
{
// Not a String value
return false;
}
}
}
else
{
for ( Value<?> val:vals )
{
if ( val instanceof BinaryValue )
{
if ( !values.contains( val ) )
{
return false;
}
}
else
{
// Not a Binary value
return false;
}
}
}
}
return true;
}
/**
* <p>
* Indicates whether the specified values are some of the attribute's values.
* </p>
* <p>
* If the Attribute is not HR, the values will be converted to byte[]
* </p>
*
* @param vals the values
* @return true if this attribute contains all the values, otherwise false
*/
public boolean contains( String... vals )
{
if ( isHR == null )
{
// If this flag is null, then there is no values.
return false;
}
if ( attributeType == null )
{
if ( isHR )
{
for ( String val:vals )
{
if ( !contains( new StringValue( val ) ) )
{
return false;
}
}
}
else
{
// As the attribute type is binary, we have to convert
// the values before checking for them in the values
// Iterate through all the values, and quit if we
// don't find one in the values
for ( String val:vals )
{
byte[] binaryVal = Strings.getBytesUtf8(val);
if ( !contains( new BinaryValue( binaryVal ) ) )
{
return false;
}
}
}
}
else
{
if ( isHR )
{
// Iterate through all the values, and quit if we
// don't find one in the values
for ( String val:vals )
{
StringValue value = new StringValue( attributeType, val );
if ( !values.contains( value ) )
{
return false;
}
}
return true;
}
else
{
return false;
}
}
return true;
}
/**
* <p>
* Indicates whether the specified values are some of the attribute's values.
* </p>
* <p>
* If the Attribute is HR, the values will be converted to String
* </p>
*
* @param vals the values
* @return true if this attribute contains all the values, otherwise false
*/
public boolean contains( byte[]... vals )
{
if ( isHR == null )
{
// If this flag is null, then there is no values.
return false;
}
if ( attributeType == null )
{
if ( !isHR )
{
// Iterate through all the values, and quit if we
// don't find one in the values
for ( byte[] val:vals )
{
if ( !contains( new BinaryValue( val ) ) )
{
return false;
}
}
}
else
{
// As the attribute type is String, we have to convert
// the values before checking for them in the values
// Iterate through all the values, and quit if we
// don't find one in the values
for ( byte[] val:vals )
{
String stringVal = Strings.utf8ToString(val);
if ( !contains( new StringValue( stringVal ) ) )
{
return false;
}
}
}
}
else
{
if ( !isHR )
{
// Iterate through all the values, and quit if we
// don't find one in the values
for ( byte[] val:vals )
{
BinaryValue value = new BinaryValue( attributeType, val );
try
{
value.normalize();
}
catch ( LdapException ne )
{
return false;
}
if ( !values.contains( value ) )
{
return false;
}
}
return true;
}
else
{
return false;
}
}
return true;
}
/**
* @see EntryAttribute#contains(Object...)
*/
public boolean contains( Object... vals )
{
boolean isHR = true;
boolean seen = false;
// Iterate through all the values, and quit if we
// don't find one in the values
for ( Object val:vals )
{
if ( ( val instanceof String ) )
{
if ( !seen )
{
isHR = true;
seen = true;
}
if ( isHR )
{
if ( !contains( (String)val ) )
{
return false;
}
}
else
{
return false;
}
}
else
{
if ( !seen )
{
isHR = false;
seen = true;
}
if ( !isHR )
{
if ( !contains( (byte[])val ) )
{
return false;
}
}
else
{
return false;
}
}
}
return true;
}
/**
* <p>
* Get the first value of this attribute. If there is none,
* null is returned.
* </p>
* <p>
* Note : even if we are storing values into a Set, one can assume
* the values are ordered following the insertion order.
* </p>
* <p>
* This method is meant to be used if the attribute hold only one value.
* </p>
*
* @return The first value for this attribute.
*/
public Value<?> get()
{
if ( values.isEmpty() )
{
return null;
}
return values.iterator().next();
}
/**
* <p>
* Get the nth value of this attribute. If there is none,
* null is returned.
* </p>
* <p>
* Note : even if we are storing values into a Set, one can assume
* the values are ordered following the insertion order.
* </p>
* <p>
*
* @param i the index of the value to get
* @return The nth value for this attribute.
*/
public Value<?> get( int i )
{
if ( values.size() < i )
{
return null;
}
else
{
int n = 0;
for ( Value<?> value:values )
{
if ( n == i )
{
return value;
}
n++;
}
}
// fallback to
return null;
}
/**
* Returns an iterator over all the attribute's values.
* <p>
* The effect on the returned enumeration of adding or removing values of
* the attribute is not specified.
* </p>
* <p>
* This method will throw any <code>LdapException</code> that occurs.
* </p>
*
* @return an enumeration of all values of the attribute
*/
public Iterator<Value<?>> getAll()
{
return iterator();
}
/**
* Retrieves the number of values in this attribute.
*
* @return the number of values in this attribute, including any values
* wrapping a null value if there is one
*/
public int size()
{
return values.size();
}
/**
* <p>
* Removes all the values that are equal to the given values.
* </p>
* <p>
* Returns true if all the values are removed.
* </p>
* <p>
* If the attribute type is HR and some value which are not String, we
* will convert the values first (same thing for a non-HR attribute).
* </p>
*
* @param vals the values to be removed
* @return true if all the values are removed, otherwise false
*/
public boolean remove( Value<?>... vals )
{
if ( ( isHR == null ) || ( values.size() == 0 ) )
{
// Trying to remove a value from an empty list will fail
return false;
}
boolean removed = true;
if ( attributeType == null )
{
if ( isHR )
{
for ( Value<?> val:vals )
{
if ( val instanceof StringValue)
{
removed &= values.remove( val );
}
else
{
// Convert the binary value to a string value
byte[] binaryVal = val.getBytes();
removed &= values.remove( new StringValue( Strings.utf8ToString(binaryVal) ) );
}
}
}
else
{
for ( Value<?> val:vals )
{
removed &= values.remove( val );
}
}
}
else
{
// Loop through all the values to remove. If one of
// them is not present, the method will return false.
// As the attribute may be HR or not, we have two separated treatments
if ( isHR )
{
for ( Value<?> val:vals )
{
if ( val instanceof StringValue)
{
StringValue stringValue = (StringValue)val;
if ( stringValue.getAttributeType() == null )
{
stringValue.apply( attributeType );
}
removed &= values.remove( stringValue );
}
else
{
removed = false;
}
}
}
else
{
for ( Value<?> val:vals )
{
if ( val instanceof BinaryValue )
{
BinaryValue binaryValue = (BinaryValue)val;
if ( binaryValue.getAttributeType() == null )
{
binaryValue.apply( attributeType );
}
removed &= values.remove( binaryValue );
}
else
{
removed = false;
}
}
}
}
return removed;
}
/**
* <p>
* Removes all the values that are equal to the given values.
* </p>
* <p>
* Returns true if all the values are removed.
* </p>
* <p>
* If the attribute type is HR, then the values will be first converted
* to String
* </p>
*
* @param vals the values to be removed
* @return true if all the values are removed, otherwise false
*/
public boolean remove( byte[]... vals )
{
if ( ( isHR == null ) || ( values.size() == 0 ) )
{
// Trying to remove a value from an empty list will fail
return false;
}
boolean removed = true;
if ( attributeType == null )
{
if ( !isHR )
{
// The attribute type is not HR, we can directly process the values
for ( byte[] val:vals )
{
BinaryValue value = new BinaryValue( val );
removed &= values.remove( value );
}
}
else
{
// The attribute type is String, we have to convert the values
// to String before removing them
for ( byte[] val:vals )
{
StringValue value = new StringValue( Strings.utf8ToString(val) );
removed &= values.remove( value );
}
}
}
else
{
if ( !isHR )
{
for ( byte[] val:vals )
{
BinaryValue value = new BinaryValue( attributeType, val );
removed &= values.remove( value );
}
}
else
{
removed = false;
}
}
return removed;
}
/**
* Removes all the values that are equal to the given values.
* <p>
* Returns true if all the values are removed.
* </p>
* <p>
* If the attribute type is not HR, then the values will be first converted
* to byte[]
* </p>
*
* @param vals the values to be removed
* @return true if all the values are removed, otherwise false
*/
public boolean remove( String... vals )
{
if ( ( isHR == null ) || ( values.size() == 0 ) )
{
// Trying to remove a value from an empty list will fail
return false;
}
boolean removed = true;
if ( attributeType == null )
{
if ( isHR )
{
// The attribute type is HR, we can directly process the values
for ( String val:vals )
{
StringValue value = new StringValue( val );
removed &= values.remove( value );
}
}
else
{
// The attribute type is binary, we have to convert the values
// to byte[] before removing them
for ( String val:vals )
{
BinaryValue value = new BinaryValue( Strings.getBytesUtf8(val) );
removed &= values.remove( value );
}
}
}
else
{
if ( isHR )
{
for ( String val:vals )
{
StringValue value = new StringValue( attributeType, val );
removed &= values.remove( value );
}
}
else
{
removed = false;
}
}
return removed;
}
/**
* An iterator on top of the stored values.
*
* @return an iterator over the stored values.
*/
public Iterator<Value<?>> iterator()
{
return values.iterator();
}
/*
* Puts some values to this attribute.
* <p>
* The new values will replace the previous values.
* </p>
* <p>
* This method returns the number of values that were put.
* </p>
*
* @param val some values to be put which may be null
* @return the number of added values, or 0 if none has been added
*
public int put( String... vals )
{
values.clear();
return add( vals );
}
/**
* Puts some values to this attribute.
* <p>
* The new values will replace the previous values.
* </p>
* <p>
* This method returns the number of values that were put.
* </p>
*
* @param val some values to be put which may be null
* @return the number of added values, or 0 if none has been added
*
public int put( byte[]... vals )
{
values.clear();
return add( vals );
}
/**
* Puts some values to this attribute.
* <p>
* The new values are replace the previous values.
* </p>
* <p>
* This method returns the number of values that were put.
* </p>
*
* @param val some values to be put which may be null
* @return the number of added values, or 0 if none has been added
*
public int put( Value<?>... vals )
{
values.clear();
return add( vals );
}
/**
* <p>
* Puts a list of values into this attribute.
* </p>
* <p>
* The new values will replace the previous values.
* </p>
* <p>
* This method returns the number of values that were put.
* </p>
*
* @param vals the values to be put
* @return the number of added values, or 0 if none has been added
*
public int put( List<Value<?>> vals )
{
values.clear();
// Transform the List to an array
Value<?>[] valArray = new Value<?>[vals.size()];
return add( vals.toArray( valArray ) );
}
*/
/**
* Get the attribute type associated with this ServerAttribute.
*
* @return the attributeType associated with this entry attribute
*/
public AttributeType getAttributeType()
{
return attributeType;
}
/**
* <p>
* Set the attribute type associated with this ServerAttribute.
* </p>
* <p>
* The current attributeType will be replaced. It is the responsibility of
* the caller to insure that the existing values are compatible with the new
* AttributeType
* </p>
*
* @param attributeType the attributeType associated with this entry attribute
*/
public void setAttributeType( AttributeType attributeType )
{
if ( attributeType == null )
{
throw new IllegalArgumentException( "The AttributeType parameter should not be null" );
}
this.attributeType = attributeType;
setUpId( null, attributeType );
isHR = attributeType.getSyntax().isHumanReadable();
// Compute the hashCode
rehash();
}
/**
* <p>
* Check if the current attribute type is of the expected attributeType
* </p>
* <p>
* This method won't tell if the current attribute is a descendant of
* the attributeType. For instance, the "CN" serverAttribute will return
* false if we ask if it's an instance of "Name".
* </p>
*
* @param attributeId The AttributeType ID to check
* @return True if the current attribute is of the expected attributeType
* @throws LdapInvalidAttributeValueException If there is no AttributeType
*/
public boolean instanceOf( String attributeId ) throws LdapInvalidAttributeValueException
{
String trimmedId = Strings.trim(attributeId);
if ( Strings.isEmpty(trimmedId) )
{
return false;
}
String normId = Strings.lowerCaseAscii(trimmedId);
for ( String name:attributeType.getNames() )
{
if ( normId.equalsIgnoreCase( name ) )
{
return true;
}
}
return normId.equalsIgnoreCase( attributeType.getOid() );
}
//-------------------------------------------------------------------------
// Overloaded Object classes
//-------------------------------------------------------------------------
/**
* A helper method to rehash the hashCode
*/
private void rehash()
{
h = 37;
if ( isHR != null )
{
h = h*17 + isHR.hashCode();
}
if ( id != null )
{
h = h*17 + id.hashCode();
}
/*
// We have to sort the values if we wnt to correctly compare two Attributes
if ( isHR )
{
SortedSet<String> sortedSet = new TreeSet<String>();
for ( Value<?> value:values )
{
sortedSet.add( (String)value.getNormalizedValueReference() );
}
for ( String value:sortedSet )
{
h = h*17 + value.hashCode();
}
}
else
{
SortedSet<byte[]> sortedSet = new TreeSet<byte[]>();
for ( Value<?> value:values )
{
sortedSet.add( (byte[])value.getNormalizedValueReference() );
}
for ( byte[] value:sortedSet )
{
h = h*17 + ArrayUtils.hashCode( value );
}
}
*/
if ( attributeType != null )
{
h = h*17 + attributeType.hashCode();
}
}
/**
* The hashCode is based on the id, the isHR flag and
* on the internal values.
*
* @see Object#hashCode()
* @return the instance's hashcode
*/
public int hashCode()
{
if ( h == 0 )
{
rehash();
}
return h;
}
/**
* @see Object#equals(Object)
*/
public boolean equals( Object obj )
{
if ( obj == this )
{
return true;
}
if ( ! (obj instanceof EntryAttribute ) )
{
return false;
}
EntryAttribute other = (EntryAttribute)obj;
if ( id == null )
{
if ( other.getId() != null )
{
return false;
}
}
else
{
if ( other.getId() == null )
{
return false;
}
else
{
if ( attributeType != null )
{
if ( !attributeType.equals( other.getAttributeType() ) )
{
return false;
}
}
else if ( !id.equals( other.getId() ) )
{
return false;
}
}
}
if ( isHR() != other.isHR() )
{
return false;
}
if ( values.size() != other.size() )
{
return false;
}
for ( Value<?> val:values )
{
if ( ! other.contains( val ) )
{
return false;
}
}
if ( attributeType == null )
{
return other.getAttributeType() == null;
}
return attributeType.equals( other.getAttributeType() );
}
/**
* @see Cloneable#clone()
*/
public EntryAttribute clone()
{
try
{
DefaultEntryAttribute attribute = (DefaultEntryAttribute)super.clone();
attribute.setUpId( upId );
attribute.values = new LinkedHashSet<Value<?>>( values.size() );
for ( Value<?> value:values )
{
attribute.values.add( value.clone() );
}
return attribute;
}
catch ( CloneNotSupportedException cnse )
{
return null;
}
}
/**
* @see Object#toString()
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
if ( ( values != null ) && ( values.size() != 0 ) )
{
for ( Value<?> value:values )
{
sb.append( " " ).append( upId ).append( ": " );
if ( value.isNull() )
{
sb.append( "''" );
}
else
{
sb.append( value );
}
sb.append( '\n' );
}
}
else
{
sb.append( " " ).append( upId ).append( ": (null)\n" );
}
return sb.toString();
}
/**
* This is the place where we serialize attributes, and all theirs
* elements.
*
* The inner structure is the same as the client attribute, but we can't call
* it as we won't be able to serialize the serverValues
*/
public void serialize( ObjectOutput out ) throws IOException
{
// Write the UPId (the id will be deduced from the upID)
Unicode.writeUTF(out, upId);
// Write the HR flag, if not null
if ( isHR != null )
{
out.writeBoolean( true );
out.writeBoolean( isHR );
}
else
{
out.writeBoolean( false );
}
// Write the number of values
out.writeInt( size() );
if ( size() > 0 )
{
// Write each value
for ( Value<?> value:values )
{
// Write the value, using the correct method
if ( value instanceof StringValue)
{
StringValue.serialize( value, out );
}
else
{
BinaryValue.serialize( value, out );
}
}
}
}
/**
* {@inheritDoc}
*/
public void deserialize( ObjectInput in ) throws IOException, ClassNotFoundException
{
// Read the ID and the UPId
upId = Unicode.readUTF(in);
// Compute the id
setUpId( upId );
// Read the HR flag, if not null
if ( in.readBoolean() )
{
isHR = in.readBoolean();
}
// Read the number of values
int nbValues = in.readInt();
if ( nbValues > 0 )
{
for ( int i = 0; i < nbValues; i++ )
{
Value<?> value = null;
if ( isHR )
{
value = StringValue.deserialize( null, in );
}
else
{
value = BinaryValue.deserialize( null, in );
}
try
{
value.normalize();
}
catch ( LdapException ne )
{
// Do nothing...
}
values.add( value );
}
}
}
/**
* This is the place where we serialize attributes, and all theirs
* elements.
*
* The inner structure is :
*
* {@inheritDoc}
*/
public void writeExternal( ObjectOutput out ) throws IOException
{
// Write the UPId (the id will be deduced from the upID)
Unicode.writeUTF(out, upId);
// Write the HR flag, if not null
if ( isHR != null )
{
out.writeBoolean( true );
out.writeBoolean( isHR );
}
else
{
out.writeBoolean( false );
}
// Write the number of values
out.writeInt( size() );
if ( size() > 0 )
{
// Write each value
for ( Value<?> value:values )
{
// Write the value
out.writeObject( value );
}
}
out.flush();
}
/**
* {@inheritDoc}
*/
public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
{
// Read the ID and the UPId
upId = Unicode.readUTF(in);
// Compute the id
setUpId( upId );
// Read the HR flag, if not null
if ( in.readBoolean() )
{
isHR = in.readBoolean();
}
// Read the number of values
int nbValues = in.readInt();
if ( nbValues > 0 )
{
for ( int i = 0; i < nbValues; i++ )
{
values.add( (Value<?>)in.readObject() );
}
}
}
}