blob: bbd189af7dc186a3d888b5231621d51ab8d0d5ca [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
*
* https://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.api.ldap.model.name;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Chars;
import org.apache.directory.api.util.Hex;
import org.apache.directory.api.util.Serialize;
import org.apache.directory.api.util.Strings;
import org.apache.directory.api.util.Unicode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class store the name-component part or the following BNF grammar (as of
* RFC2253, par. 3, and RFC1779, fig. 1) : <br> - &lt;name-component&gt; ::=
* &lt;attributeType&gt; &lt;spaces&gt; '=' &lt;spaces&gt;
* &lt;attributeValue&gt; &lt;attributeTypeAndValues&gt; <br> -
* &lt;attributeTypeAndValues&gt; ::= &lt;spaces&gt; '+' &lt;spaces&gt;
* &lt;attributeType&gt; &lt;spaces&gt; '=' &lt;spaces&gt;
* &lt;attributeValue&gt; &lt;attributeTypeAndValues&gt; | e <br> -
* &lt;attributeType&gt; ::= [a-zA-Z] &lt;keychars&gt; | &lt;oidPrefix&gt; [0-9]
* &lt;digits&gt; &lt;oids&gt; | [0-9] &lt;digits&gt; &lt;oids&gt; <br> -
* &lt;keychars&gt; ::= [a-zA-Z] &lt;keychars&gt; | [0-9] &lt;keychars&gt; | '-'
* &lt;keychars&gt; | e <br> - &lt;oidPrefix&gt; ::= 'OID.' | 'oid.' | e <br> -
* &lt;oids&gt; ::= '.' [0-9] &lt;digits&gt; &lt;oids&gt; | e <br> -
* &lt;attributeValue&gt; ::= &lt;pairs-or-strings&gt; | '#' &lt;hexstring&gt;
* |'"' &lt;quotechar-or-pairs&gt; '"' <br> - &lt;pairs-or-strings&gt; ::= '\'
* &lt;pairchar&gt; &lt;pairs-or-strings&gt; | &lt;stringchar&gt;
* &lt;pairs-or-strings&gt; | e <br> - &lt;quotechar-or-pairs&gt; ::=
* &lt;quotechar&gt; &lt;quotechar-or-pairs&gt; | '\' &lt;pairchar&gt;
* &lt;quotechar-or-pairs&gt; | e <br> - &lt;pairchar&gt; ::= ',' | '=' | '+' |
* '&lt;' | '&gt;' | '#' | ';' | '\' | '"' | [0-9a-fA-F] [0-9a-fA-F] <br> -
* &lt;hexstring&gt; ::= [0-9a-fA-F] [0-9a-fA-F] &lt;hexpairs&gt; <br> -
* &lt;hexpairs&gt; ::= [0-9a-fA-F] [0-9a-fA-F] &lt;hexpairs&gt; | e <br> -
* &lt;digits&gt; ::= [0-9] &lt;digits&gt; | e <br> - &lt;stringchar&gt; ::=
* [0x00-0xFF] - [,=+&lt;&gt;#;\"\n\r] <br> - &lt;quotechar&gt; ::= [0x00-0xFF] -
* [\"] <br> - &lt;separator&gt; ::= ',' | ';' <br> - &lt;spaces&gt; ::= ' '
* &lt;spaces&gt; | e <br>
* <br>
* A Rdn is a part of a Dn. It can be composed of many types, as in the Rdn
* following Rdn :<br>
* ou=value + cn=other value<br>
* <br>
* or <br>
* ou=value + ou=another value<br>
* <br>
* In this case, we have to store an 'ou' and a 'cn' in the Rdn.<br>
* <br>
* The types are case insensitive. <br>
* Spaces before and after types and values are not stored.<br>
* Spaces before and after '+' are not stored.<br>
* <br>
* Thus, we can consider that the following RDNs are equals :<br>
* <br>
* 'ou=test 1'<br> ' ou=test 1'<br>
* 'ou =test 1'<br>
* 'ou= test 1'<br>
* 'ou=test 1 '<br> ' ou = test 1 '<br>
* <br>
* So are the following :<br>
* <br>
* 'ou=test 1+cn=test 2'<br>
* 'ou = test 1 + cn = test 2'<br> ' ou =test 1+ cn =test 2 ' <br>
* 'cn = test 2 +ou = test 1'<br>
* <br>
* but the following are not equal :<br>
* 'ou=test 1' <br>
* 'ou=test 1'<br>
* because we have more than one spaces inside the value.<br>
* <br>
* The Rdn is composed of one or more Ava. Those Avas
* are ordered in the alphabetical natural order : a &lt; b &lt; c ... &lt; z As the type
* are not case sensitive, we can say that a = A
* <br>
* This class is immutable.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class Rdn implements Cloneable, Externalizable, Iterable<Ava>, Comparable<Rdn>
{
/** The LoggerFactory used by this class */
protected static final Logger LOG = LoggerFactory.getLogger( Rdn.class );
/** An empty Rdn */
public static final Rdn EMPTY_RDN = new Rdn();
/**
* Declares the Serial Version Uid.
*
* @see <a
* href="http://c2.com/cgi/wiki?AlwaysDeclareSerialVersionUid">Always
* Declare Serial Version Uid</a>
*/
private static final long serialVersionUID = 1L;
/** The User Provided Rdn */
private String upName = null;
/** The normalized Rdn */
private String normName;
/**
* Stores all couple type = value. We may have more than one type, if the
* '+' character appears in the Ava. This is a TreeSet,
* because we want the Avas to be sorted. An Ava may contain more than one
* value. In this case, the values are String stored in a List.
*/
private transient List<Ava> avas = null;
/**
* We also keep a set of types, in order to use manipulations. A type is
* connected with the Ava it represents.
*/
private transient Map<String, List<Ava>> avaTypes;
/**
* We keep the type for a single valued Rdn, to avoid the creation of an HashMap
*/
private String avaType = null;
/**
* A simple Ava is used to store the Rdn for the simple
* case where we only have a single type=value. This will be 99.99% the
* case. This avoids the creation of a HashMap.
*/
protected Ava ava = null;
/**
* The number of Avas. We store this number here to avoid complex
* manipulation of Ava and Avas
*/
private int nbAvas = 0;
/** CompareTo() results */
public static final int UNDEFINED = Integer.MAX_VALUE;
/** Constant used in comparisons */
public static final int SUPERIOR = 1;
/** Constant used in comparisons */
public static final int INFERIOR = -1;
/** Constant used in comparisons */
public static final int EQUAL = 0;
/** A flag used to tell if the Rdn has been normalized */
private boolean normalized = false;
/** the schema manager */
private transient SchemaManager schemaManager;
/** The computed hashcode */
private volatile int h;
/**
* A empty constructor.
*/
public Rdn()
{
this( ( SchemaManager ) null );
}
/**
*
* Creates a new schema aware instance of Rdn.
*
* @param schemaManager the schema manager
*/
public Rdn( SchemaManager schemaManager )
{
// Don't waste space... This is not so often we have multiple
// name-components in a Rdn... So we won't initialize the Map and the
// treeSet.
this.schemaManager = schemaManager;
upName = "";
normName = "";
normalized = schemaManager != null;
h = 0;
}
/**
* A constructor that parse a String representing a schema aware Rdn.
*
* @param schemaManager the schema manager
* @param rdn the String containing the Rdn to parse
* @throws LdapInvalidDnException if the Rdn is invalid
*/
public Rdn( SchemaManager schemaManager, String rdn ) throws LdapInvalidDnException
{
if ( Strings.isNotEmpty( rdn ) )
{
// Parse the string. The Rdn will be updated.
parse( schemaManager, rdn, this );
if ( upName.length() < rdn.length() )
{
throw new LdapInvalidDnException( I18n.err( I18n.ERR_13625_INVALID_RDN ) );
}
upName = rdn;
}
else
{
upName = "";
normName = "";
normalized = true;
}
hashCode();
}
/**
* A constructor that parse a String representing a Rdn.
*
* @param rdn the String containing the Rdn to parse
* @throws LdapInvalidDnException if the Rdn is invalid
*/
public Rdn( String rdn ) throws LdapInvalidDnException
{
this( ( SchemaManager ) null, rdn );
}
/**
* A constructor that constructs a schema aware Rdn from a type and a value.
* <p>
* The string attribute values are not interpreted as RFC 414 formatted Rdn
* strings. That is, the values are used literally (not parsed) and assumed
* to be un-escaped.
*
* @param schemaManager the schema manager
* @param upType the user provided type of the Rdn
* @param upValue the user provided value of the Rdn
* @throws LdapInvalidDnException if the Rdn is invalid
* @throws LdapInvalidAttributeValueException If the given AttributeType or value are invalid
*/
public Rdn( SchemaManager schemaManager, String upType, String upValue ) throws LdapInvalidDnException, LdapInvalidAttributeValueException
{
if ( schemaManager != null )
{
AttributeType attributeType = schemaManager.getAttributeType( upType );
addAVA( schemaManager, upType, new Value( attributeType, upValue ) );
}
else
{
addAVA( schemaManager, upType, new Value( upValue ) );
}
StringBuilder sb = new StringBuilder();
sb.append( upType ).append( '=' ).append( upValue );
upName = sb.toString();
sb.setLength( 0 );
sb.append( ava.getNormType() ).append( '=' );
Value value = ava.getValue();
if ( value != null )
{
sb.append( value.getNormalized() );
}
normName = sb.toString();
normalized = true;
hashCode();
}
/**
* A constructor that constructs a Rdn from a type and a value.
*
* @param upType the user provided type of the Rdn
* @param upValue the user provided value of the Rdn
* @throws LdapInvalidDnException if the Rdn is invalid
* @throws LdapInvalidAttributeValueException If the given AttributeType or Value are incorrect
* @see #Rdn( SchemaManager, String, String )
*/
public Rdn( String upType, String upValue ) throws LdapInvalidDnException, LdapInvalidAttributeValueException
{
this( null, upType, upValue );
}
/**
* Creates a new schema aware RDN from a list of AVA
*
* @param schemaManager The schemaManager to use
* @param avas The AVA that will be used
* @throws LdapInvalidDnException If the RDN is invalid
*/
public Rdn( SchemaManager schemaManager, Ava... avas ) throws LdapInvalidDnException
{
StringBuilder buffer = new StringBuilder();
for ( int i = 0; i < avas.length; i++ )
{
if ( i > 0 )
{
buffer.append( '+' );
}
addAVA( schemaManager, avas[i] );
buffer.append( avas[i].getName() );
}
setUpName( buffer.toString() );
hashCode();
}
/**
* Creates a new RDN from a list of AVA
*
* @param avas The AVA that will be used
* @throws LdapInvalidDnException If the RDN is invalid
*/
public Rdn( Ava... avas ) throws LdapInvalidDnException
{
this( null, avas );
}
/**
* Constructs an Rdn from the given rdn. The content of the rdn is simply
* copied into the newly created Rdn.
*
* @param rdn The non-null Rdn to be copied.
*/
public Rdn( Rdn rdn )
{
nbAvas = rdn.size();
upName = rdn.getName();
normName = rdn.getName();
normalized = rdn.normalized;
schemaManager = rdn.schemaManager;
switch ( rdn.size() )
{
case 0:
hashCode();
return;
case 1:
this.ava = rdn.ava.clone();
hashCode();
return;
default:
// We must duplicate the treeSet and the hashMap
avas = new ArrayList<>();
avaTypes = new HashMap<>();
for ( Ava currentAva : rdn.avas )
{
avas.add( currentAva );
List<Ava> avaList = avaTypes.get( currentAva.getNormType() );
if ( avaList == null )
{
avaList = new ArrayList<>();
avaList.add( currentAva );
avaTypes.put( currentAva.getNormType(), avaList );
avas.add( currentAva );
}
else
{
if ( !avaList.contains( currentAva ) )
{
avaList.add( currentAva );
avas.add( currentAva );
}
}
}
hashCode();
return;
}
}
/**
* Constructs an Rdn from the given rdn. The content of the rdn is simply
* copied into the newly created Rdn.
*
* @param schemaManager The SchemaManager
* @param rdn The non-null Rdn to be copied.
* @throws LdapInvalidDnException If the given Rdn is invalid
*/
public Rdn( SchemaManager schemaManager, Rdn rdn ) throws LdapInvalidDnException
{
nbAvas = rdn.size();
this.upName = rdn.getName();
this.schemaManager = schemaManager;
normalized = rdn.normalized;
switch ( rdn.size() )
{
case 0:
hashCode();
return;
case 1:
ava = new Ava( schemaManager, rdn.ava );
StringBuilder sb = new StringBuilder();
sb.append( ava.getNormType() );
sb.append( '=' );
if ( ( ava.getValue() != null ) && ( ava.getValue().getNormalized() != null ) )
{
sb.append( ava.getValue().getNormalized() );
}
normName = sb.toString();
normalized = true;
hashCode();
return;
default:
// We must duplicate the treeSet and the hashMap
avas = new ArrayList<>();
avaTypes = new HashMap<>();
sb = new StringBuilder();
boolean isFirst = true;
for ( Ava currentAva : rdn.avas )
{
Ava tmpAva = currentAva;
if ( !currentAva.isSchemaAware() && ( schemaManager != null ) )
{
tmpAva = new Ava( schemaManager, currentAva );
}
List<Ava> avaList = avaTypes.get( tmpAva.getNormType() );
boolean empty = avaList == null;
avaList = addOrdered( avaList, tmpAva );
if ( empty )
{
avaTypes.put( tmpAva.getNormType(), avaList );
}
addOrdered( avas, tmpAva );
}
for ( Ava ava : avas )
{
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( '+' );
}
sb.append( ava.getNormType() );
sb.append( '=' );
if ( ( ava.getValue() != null ) && ( ava.getValue().getNormalized() != null ) )
{
sb.append( ava.getValue().getNormalized() );
}
}
normName = sb.toString();
normalized = true;
hashCode();
return;
}
}
/**
* Add an AVA in a List of Ava, at the right place (ordered)
*
* @param avaList The list of Ava
* @param newAva The Ava to add
* @return The list of Ava with the new Ava at the right position
*/
private List<Ava> addOrdered( List<Ava> avaList, Ava newAva )
{
if ( avaList == null )
{
avaList = new ArrayList<>();
}
if ( avaList.isEmpty() )
{
avaList.add( newAva );
return avaList;
}
// Insert the AVA in the list, ordered.
int pos = 0;
boolean found = false;
for ( Ava avaElem : avaList )
{
int comp = newAva.compareTo( avaElem );
if ( comp < 0 )
{
avaList.add( pos, newAva );
found = true;
break;
}
else if ( comp == 0 )
{
found = true;
break;
}
else
{
pos++;
}
}
if ( !found )
{
avaList.add( newAva );
}
return avaList;
}
/**
* Add an Ava to the current Rdn
*
* @param schemaManager The {@link SchemaManager}
* @param type The user provided type of the added Rdn.
* @param value The user provided provided value of the added Rdn
* @throws LdapInvalidDnException If the Rdn is invalid
*/
private void addAVA( SchemaManager schemaManager, String type, Value value ) throws LdapInvalidDnException
{
// First, let's normalize the type
AttributeType attributeType;
String normalizedType = Strings.lowerCaseAscii( type );
this.schemaManager = schemaManager;
if ( schemaManager != null )
{
attributeType = schemaManager.getAttributeType( normalizedType );
if ( !value.isSchemaAware() )
{
if ( attributeType != null )
{
try
{
value = new Value( attributeType, value );
}
catch ( LdapInvalidAttributeValueException liave )
{
throw new LdapInvalidDnException( liave.getMessage(), liave );
}
}
}
else
{
if ( attributeType != null )
{
normalizedType = attributeType.getOid();
}
}
}
Ava newAva = new Ava( schemaManager, type, normalizedType, value );
switch ( nbAvas )
{
case 0:
// This is the first Ava. Just stores it.
ava = newAva;
nbAvas = 1;
avaType = normalizedType;
hashCode();
return;
case 1:
// We already have an Ava. We have to put it in the HashMap
// before adding a new one, if it's not already present
if ( ava.equals( newAva ) )
{
return;
}
// First, create the List and the HashMap
avas = new ArrayList<>();
avaTypes = new HashMap<>();
List<Ava> avaList = new ArrayList<>();
// and store the existing Ava into it.
avas.add( ava );
avaList.add( ava );
avaTypes.put( avaType, avaList );
nbAvas++;
ava = null;
// Now, fall down to the commmon case
// NO BREAK !!!
default:
// add a new Ava, if it's not already present
avaList = avaTypes.get( newAva.getNormType() );
if ( avaList == null )
{
// Not present, we can add it
avaList = new ArrayList<>();
avaList.add( newAva );
avaTypes.put( newAva.getNormType(), avaList );
avas.add( newAva );
nbAvas++;
}
else
{
// We have at least one Ava with the same type, check if it's the same value
if ( !avaList.contains( newAva ) )
{
// Ok, we can add it
avaList.add( newAva );
avas.add( newAva );
nbAvas++;
}
}
}
}
/**
* Add an Ava to the current schema aware Rdn
*
* @param schemaManager The SchemaManager
* @param addedAva The added Ava
* @throws LdapInvalidDnException If the Ava is invalid
*/
// WARNING : The protection level is left unspecified intentionally.
// We need this method to be visible from the DnParser class, but not
// from outside this package.
/* Unspecified protection */void addAVA( SchemaManager schemaManager, Ava addedAva ) throws LdapInvalidDnException
{
this.schemaManager = schemaManager;
if ( !addedAva.isSchemaAware() && ( schemaManager != null ) )
{
addedAva = new Ava( schemaManager, addedAva );
}
String normalizedType = addedAva.getNormType();
switch ( nbAvas )
{
case 0:
// This is the first Ava. Just stores it.
ava = addedAva;
nbAvas = 1;
avaType = normalizedType;
hashCode();
return;
case 1:
// We already have an Ava. We have to put it in the HashMap
// before adding a new one.
// Check that the first AVA is not for the same attribute
if ( ava.equals( addedAva ) )
{
throw new LdapInvalidDnException( I18n.err( I18n.ERR_13626_INVALID_RDN_DUPLICATE_AVA, normalizedType ) );
}
// First, create the List and the hashMap
avas = new ArrayList<>();
avaTypes = new HashMap<>();
List<Ava> avaList = new ArrayList<>();
// and store the existing Ava into it.
avas.add( ava );
avaList.add( ava );
avaTypes.put( ava.getNormType(), avaList );
this.ava = null;
// Now, fall down to the commmon case
// NO BREAK !!!
default:
// Check that the AT is not already present
avaList = avaTypes.get( addedAva.getNormType() );
if ( avaList == null )
{
// Not present, we can add it
avaList = new ArrayList<>();
avaList.add( addedAva );
avaTypes.put( addedAva.getNormType(), avaList );
avas.add( addedAva );
nbAvas++;
}
else
{
// We have at least one Ava with the same type, check if it's the same value
addOrdered( avaList, addedAva );
boolean found = false;
for ( int pos = 0; pos < avas.size(); pos++ )
{
int comp = addedAva.compareTo( avas.get( pos ) );
if ( comp < 0 )
{
avas.add( pos, addedAva );
found = true;
nbAvas++;
break;
}
else if ( comp == 0 )
{
found = true;
break;
}
}
// Ok, we can add it at the end if we haven't already added it
if ( !found )
{
avas.add( addedAva );
nbAvas++;
}
}
break;
}
}
/**
* Clear the Rdn, removing all the Avas.
*/
// WARNING : The protection level is left unspecified intentionally.
// We need this method to be visible from the DnParser class, but not
// from outside this package.
/* No protection */void clear()
{
ava = null;
avas = null;
avaType = null;
avaTypes = null;
nbAvas = 0;
upName = "";
normalized = false;
h = 0;
}
/**
* Get the value of the Ava which type is given as an
* argument.
*
* @param type the type of the NameArgument
* @return the value to be returned, or null if none found.
* @throws LdapInvalidDnException if the Rdn is invalid
*/
public Object getValue( String type ) throws LdapInvalidDnException
{
// First, let's normalize the type
String normalizedType = Strings.lowerCaseAscii( Strings.trim( type ) );
if ( schemaManager != null )
{
AttributeType attributeType = schemaManager.getAttributeType( normalizedType );
if ( attributeType != null )
{
normalizedType = attributeType.getOid();
}
}
switch ( nbAvas )
{
case 0:
return "";
case 1:
if ( ava.getNormType().equals( normalizedType ) )
{
if ( ava.getValue() != null )
{
return ava.getValue().getString();
}
else
{
return null;
}
}
return "";
default:
List<Ava> avaList = avaTypes.get( normalizedType );
if ( avaList != null )
{
for ( Ava elem : avaList )
{
if ( elem.getNormType().equals( normalizedType ) )
{
if ( elem.getValue() != null )
{
return elem.getValue().getString();
}
else
{
return null;
}
}
}
return null;
}
return null;
}
}
/**
* Get the Ava which type is given as an argument. If we
* have more than one value associated with the type, we will return only
* the first one.
*
* @param type The type of the NameArgument to be returned
* @return The Ava, of null if none is found.
*/
public Ava getAva( String type )
{
// First, let's normalize the type
String normalizedType = Strings.lowerCaseAscii( Strings.trim( type ) );
switch ( nbAvas )
{
case 0:
return null;
case 1:
if ( ava.getNormType().equals( normalizedType ) )
{
return ava;
}
return null;
default:
List<Ava> avaList = avaTypes.get( normalizedType );
if ( avaList != null )
{
return avaList.get( 0 );
}
return null;
}
}
/**
* Retrieves the components of this Rdn as an iterator of Avas.
* The effect on the iterator of updates to this Rdn is undefined. If the
* Rdn has zero components, an empty (non-null) iterator is returned.
*
* @return an iterator of the components of this Rdn, each an Ava
*/
@Override
public Iterator<Ava> iterator()
{
if ( nbAvas < 2 )
{
return new Iterator<Ava>()
{
private boolean hasMoreElement = nbAvas == 1;
@Override
public boolean hasNext()
{
return hasMoreElement;
}
@Override
public Ava next()
{
Ava obj = ava;
hasMoreElement = false;
return obj;
}
@Override
public void remove()
{
// nothing to do
}
};
}
else
{
return avas.iterator();
}
}
/**
* Clone the Rdn
*
* @return A clone of the current Rdn
*/
@Override
public Rdn clone()
{
try
{
Rdn rdn = ( Rdn ) super.clone();
rdn.normalized = normalized;
// The Ava is immutable. We won't clone it
switch ( rdn.size() )
{
case 0:
break;
case 1:
rdn.ava = this.ava.clone();
rdn.avaTypes = avaTypes;
break;
default:
// We must duplicate the treeSet and the hashMap
rdn.avaTypes = new HashMap<>();
rdn.avas = new ArrayList<>();
for ( Ava currentAva : this.avas )
{
rdn.avas.add( currentAva.clone() );
List<Ava> avaList = new ArrayList<>();
for ( Ava elem : avaTypes.get( currentAva.getNormType() ) )
{
avaList.add( elem.clone() );
}
rdn.avaTypes.put( currentAva.getNormType(), avaList );
}
break;
}
return rdn;
}
catch ( CloneNotSupportedException cnse )
{
throw new Error( I18n.err( I18n.ERR_13621_ASSERTION_FAILURE ), cnse );
}
}
/**
* @return the user provided name
*/
public String getName()
{
return upName;
}
/**
* Set the User Provided Name.
*
* Package private because Rdn is immutable, only used by the Dn parser.
*
* @param upName the User Provided dame
*/
void setUpName( String upName )
{
this.upName = upName;
}
/**
* @return the normalized name
*/
public String getNormName()
{
return normName;
}
/**
* Set the normalized Name.
*
* Package private because Rdn is immutable, only used by the Dn parser.
*
* @param normName the Normalized dame
*/
void setNormName( String normName )
{
this.normName = normName;
normalized = true;
}
/**
* Return the unique Ava, or the first one of we have more
* than one
*
* @return The first Ava of this Rdn
*/
public Ava getAva()
{
switch ( nbAvas )
{
case 0:
return null;
case 1:
return ava;
default:
return avas.get( 0 );
}
}
/**
* Return the Nth Ava
*
* @param pos The Ava we are looking for
*
* @return The Ava at the given position in this Rdn
*/
public Ava getAva( int pos )
{
if ( pos > nbAvas )
{
return null;
}
if ( pos == 0 )
{
if ( nbAvas == 1 )
{
return ava;
}
else
{
return avas.get( 0 );
}
}
else
{
return avas.get( pos );
}
}
/**
* Return the user provided type, or the first one of we have more than one (the lowest)
*
* @return The first user provided type of this Rdn
*/
public String getType()
{
switch ( nbAvas )
{
case 0:
return null;
case 1:
return ava.getType();
default:
return avas.get( 0 ).getType();
}
}
/**
* Return the normalized type, or the first one of we have more than one (the lowest)
*
* @return The first normalized type of this Rdn
*/
public String getNormType()
{
switch ( nbAvas )
{
case 0:
return null;
case 1:
return ava.getNormType();
default:
return avas.get( 0 ).getNormType();
}
}
/**
* Return the User Provided value, as a String
*
* @return The first User provided value of this Rdn
*/
public String getValue()
{
switch ( nbAvas )
{
case 0:
return null;
case 1:
return ava.getValue().getString();
default:
return avas.get( 0 ).getValue().getString();
}
}
/**
* Compares the specified Object with this Rdn for equality. Returns true if
* the given object is also a Rdn and the two Rdns represent the same
* attribute type and value mappings. The order of components in
* multi-valued Rdns is not significant.
*
* @param that Rdn to be compared for equality with this Rdn
* @return true if the specified object is equal to this Rdn
*/
@Override
public boolean equals( Object that )
{
if ( this == that )
{
return true;
}
Rdn rdn;
if ( that instanceof String )
{
try
{
rdn = new Rdn( schemaManager, ( String ) that );
}
catch ( LdapInvalidDnException e )
{
return false;
}
}
else if ( !( that instanceof Rdn ) )
{
return false;
}
else
{
rdn = ( Rdn ) that;
}
if ( rdn.nbAvas != nbAvas )
{
// We don't have the same number of Avas. The Rdn which
// has the higher number of Ava is the one which is
// superior
return false;
}
switch ( nbAvas )
{
case 0:
return true;
case 1:
return ava.equals( rdn.ava );
default:
// We have more than one value. We will
// go through all of them.
// the types are already normalized and sorted in the Avas Map
// so we could compare the first element with all of the second
// Ava elements, etc.
for ( Ava paramAva : rdn.avas )
{
List<Ava> avaList = avaTypes.get( paramAva.getNormType() );
if ( ( avaList == null ) || !avaList.contains( paramAva ) )
{
return false;
}
}
return true;
}
}
/**
* Get the number of Avas of this Rdn
*
* @return The number of Avas in this Rdn
*/
public int size()
{
return nbAvas;
}
/**
* Unescape the given string according to RFC 2253 If in &lt;string&gt; form, a
* LDAP string representation asserted value can be obtained by replacing
* (left-to-right, non-recursively) each &lt;pair&gt; appearing in the &lt;string&gt; as
* follows:
* <ul>
* <li>replace &lt;ESC&gt;&lt;ESC&gt; with &lt;ESC&gt;</li>
* <li>replace &lt;ESC&gt;&lt;special&gt; with &lt;special&gt;</li>
* <li>replace &lt;ESC&gt;&lt;hexpair&gt; with the octet indicated by the &lt;hexpair&gt;</li>
* </ul>
* If in &lt;hexstring&gt; form, a BER representation can be obtained
* from converting each &lt;hexpair&gt; of the &lt;hexstring&gt; to the octet indicated
* by the &lt;hexpair&gt;
*
* @param value The value to be unescaped
* @return Returns a string value as a String, and a binary value as a byte
* array.
* @throws IllegalArgumentException When an Illegal value is provided.
*/
public static Object unescapeValue( String value )
{
if ( Strings.isEmpty( value ) )
{
return "";
}
char[] chars = value.toCharArray();
// If the value is contained into double quotes, return it as is.
if ( ( chars[0] == '\"' ) && ( chars[chars.length - 1] == '\"' ) )
{
return new String( chars, 1, chars.length - 2 );
}
if ( chars[0] == '#' )
{
if ( chars.length == 1 )
{
// The value is only containing a #
return Strings.EMPTY_BYTES;
}
if ( ( chars.length % 2 ) != 1 )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13613_VALUE_NOT_IN_HEX_FORM_ODD_NUMBER ) );
}
// HexString form
byte[] hexValue = new byte[( chars.length - 1 ) / 2];
int pos = 0;
for ( int i = 1; i < chars.length; i += 2 )
{
if ( Chars.isHex( chars, i ) && Chars.isHex( chars, i + 1 ) )
{
hexValue[pos++] = Hex.getHexValue( chars[i], chars[i + 1] );
}
else
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13614_VALUE_NOT_IN_HEX_FORM ) );
}
}
return hexValue;
}
else
{
boolean escaped = false;
boolean isHex = false;
byte pair = -1;
int pos = 0;
byte[] bytes = new byte[chars.length * 6];
for ( int i = 0; i < chars.length; i++ )
{
if ( escaped )
{
escaped = false;
switch ( chars[i] )
{
case '\\':
case '"':
case '+':
case ',':
case ';':
case '<':
case '>':
case '#':
case '=':
case ' ':
bytes[pos++] = ( byte ) chars[i];
break;
default:
if ( Chars.isHex( chars, i ) )
{
isHex = true;
pair = ( byte ) ( Hex.getHexValue( chars[i] ) << 4 );
}
break;
}
}
else
{
if ( isHex )
{
if ( Chars.isHex( chars, i ) )
{
pair += Hex.getHexValue( chars[i] );
bytes[pos++] = pair;
isHex = false;
pair = 0;
}
}
else
{
switch ( chars[i] )
{
case '\\':
escaped = true;
break;
// We must not have a special char
// Specials are : '"', '+', ',', ';', '<', '>', ' ',
// '#' and '='
case '"':
case '+':
case ',':
case ';':
case '<':
case '>':
case '#':
if ( i != 0 )
{
// '#' are allowed if not in first position
bytes[pos++] = '#';
}
break;
case ' ':
if ( ( i == 0 ) || ( i == chars.length - 1 ) )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13615_UNESCAPED_CHARS_NOT_ALLOWED ) );
}
else
{
bytes[pos++] = ' ';
break;
}
default:
if ( chars[i] < 128 )
{
bytes[pos++] = ( byte ) chars[i];
}
else
{
byte[] result = Unicode.charToBytes( chars[i] );
System.arraycopy( result, 0, bytes, pos, result.length );
pos += result.length;
}
break;
}
}
}
}
return Strings.utf8ToString( bytes, pos );
}
}
/**
* Transform a value in a String, accordingly to RFC 2253
*
* @param value The attribute value to be escaped
* @return The escaped string value.
*/
public static String escapeValue( String value )
{
if ( Strings.isEmpty( value ) )
{
return "";
}
char[] chars = value.toCharArray();
char[] newChars = new char[chars.length * 3];
int pos = 0;
for ( int i = 0; i < chars.length; i++ )
{
switch ( chars[i] )
{
case ' ':
if ( ( i > 0 ) && ( i < chars.length - 1 ) )
{
newChars[pos++] = chars[i];
}
else
{
newChars[pos++] = '\\';
newChars[pos++] = chars[i];
}
break;
case '#':
if ( i != 0 )
{
newChars[pos++] = chars[i];
}
else
{
newChars[pos++] = '\\';
newChars[pos++] = chars[i];
}
break;
case '"':
case '+':
case ',':
case ';':
case '=':
case '<':
case '>':
case '\\':
newChars[pos++] = '\\';
newChars[pos++] = chars[i];
break;
case 0x7F:
newChars[pos++] = '\\';
newChars[pos++] = '7';
newChars[pos++] = 'F';
break;
case 0x00:
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 0x07:
case 0x08:
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C:
case 0x0D:
case 0x0E:
case 0x0F:
newChars[pos++] = '\\';
newChars[pos++] = '0';
newChars[pos++] = Strings.dumpHex( ( byte ) ( chars[i] & 0x0F ) );
break;
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F:
newChars[pos++] = '\\';
newChars[pos++] = '1';
newChars[pos++] = Strings.dumpHex( ( byte ) ( chars[i] & 0x0F ) );
break;
default:
newChars[pos++] = chars[i];
break;
}
}
return new String( newChars, 0, pos );
}
/**
* @return The RDN as an escaped String
*/
public String getEscaped()
{
StringBuilder sb = new StringBuilder();
switch ( nbAvas )
{
case 0:
return "";
case 1:
sb.append( ava.getEscaped() );
break;
default:
boolean isFirst = true;
for ( Ava atav : avas )
{
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( '+' );
}
sb.append( atav.getEscaped() );
}
break;
}
return sb.toString();
}
/**
* Transform a value in a String, accordingly to RFC 2253
*
* @param attrValue The attribute value to be escaped
* @return The escaped string value.
*/
public static String escapeValue( byte[] attrValue )
{
if ( Strings.isEmpty( attrValue ) )
{
return "";
}
String value = Strings.utf8ToString( attrValue );
return escapeValue( value );
}
/**
* Tells if the Rdn is schema aware.
*
* @return <code>true</code> if the Rdn is schema aware
*/
public boolean isSchemaAware()
{
return schemaManager != null;
}
/**
* Validate a NameComponent : <br>
* <p>
* &lt;name-component&gt; ::= &lt;attributeType&gt; &lt;spaces&gt; '='
* &lt;spaces&gt; &lt;attributeValue&gt; &lt;nameComponents&gt;
* </p>
*
* @param dn The string to parse
* @return <code>true</code> if the Rdn is valid
*/
public static boolean isValid( String dn )
{
Rdn rdn = new Rdn();
try
{
parse( null, dn, rdn );
return true;
}
catch ( LdapInvalidDnException e )
{
return false;
}
}
/**
* Validate a NameComponent : <br>
* <p>
* &lt;name-component&gt; ::= &lt;attributeType&gt; &lt;spaces&gt; '='
* &lt;spaces&gt; &lt;attributeValue&gt; &lt;nameComponents&gt;
* </p>
*
* @param schemaManager The Schemamanager to use
* @param dn The string to parse
* @return <code>true</code> if the Rdn is valid
*/
public static boolean isValid( SchemaManager schemaManager, String dn )
{
Rdn rdn = new Rdn( schemaManager );
try
{
parse( schemaManager, dn, rdn );
return true;
}
catch ( LdapInvalidDnException e )
{
return false;
}
}
/**
* Parse a NameComponent : <br>
* <p>
* &lt;name-component&gt; ::= &lt;attributeType&gt; &lt;spaces&gt; '='
* &lt;spaces&gt; &lt;attributeValue&gt; &lt;nameComponents&gt;
* </p>
*
* @param schemaManager The SchemaManager
* @param dn The String to parse
* @param rdn The Rdn to fill. Beware that if the Rdn is not empty, the new
* AttributeTypeAndValue will be added.
* @throws LdapInvalidDnException If the NameComponent is invalid
*/
private static void parse( SchemaManager schemaManager, String dn, Rdn rdn ) throws LdapInvalidDnException
{
try
{
FastDnParser.parseRdn( schemaManager, dn, rdn );
}
catch ( TooComplexDnException e )
{
rdn.clear();
new ComplexDnParser().parseRdn( schemaManager, dn, rdn );
}
}
/**
* Gets the hashcode of this rdn.
*
* @see java.lang.Object#hashCode()
* @return the instance's hash code
*/
@Override
public int hashCode()
{
if ( h == 0 )
{
int hTmp = 37;
switch ( nbAvas )
{
case 0:
// An empty Rdn
break;
case 1:
// We have a single Ava
h = hTmp * 17 + ava.hashCode();
break;
default:
// We have more than one Ava
for ( Ava ata : avas )
{
h = hTmp * 17 + ata.hashCode();
}
break;
}
}
return h;
}
/**
* Serialize a RDN into a byte[]
*
* @param buffer The buffer which will contain the serilaized form of this RDN
* @param pos The position in the buffer where to store the RDN
* @return The new position in the byte[]
* @throws IOException If the serialization failed
*/
public int serialize( byte[] buffer, int pos ) throws IOException
{
// The nbAvas and the HashCode length
int length = 4 + 4;
// The NnbAvas
pos = Serialize.serialize( nbAvas, buffer, pos );
// The upName
byte[] upNameBytes = Strings.getBytesUtf8( upName );
length += 4 + upNameBytes.length;
// Check that we will be able to store the data in the buffer
if ( buffer.length - pos < length )
{
throw new ArrayIndexOutOfBoundsException();
}
// Write the upName
pos = Serialize.serialize( upNameBytes, buffer, pos );
// Write the AVAs
switch ( nbAvas )
{
case 0:
break;
case 1:
pos = ava.serialize( buffer, pos );
break;
default:
for ( Ava localAva : avas )
{
pos = localAva.serialize( buffer, pos );
}
break;
}
// The hash code
pos = Serialize.serialize( h, buffer, pos );
return pos;
}
/**
* Deserialize a RDN from a byte[], starting at a given position
*
* @param buffer The buffer containing the RDN
* @param pos The position in the buffer
* @return The new position
* @throws IOException If the serialized value is not a RDN
* @throws LdapInvalidAttributeValueException If the serialized RDN is invalid
*/
public int deserialize( byte[] buffer, int pos ) throws IOException, LdapInvalidAttributeValueException
{
if ( ( pos < 0 ) || ( pos >= buffer.length ) )
{
throw new ArrayIndexOutOfBoundsException();
}
// Read the nbAvas
nbAvas = Serialize.deserializeInt( buffer, pos );
pos += 4;
// Read the upName
byte[] upNameBytes = Serialize.deserializeBytes( buffer, pos );
pos += 4 + upNameBytes.length;
upName = Strings.utf8ToString( upNameBytes );
// Read the AVAs
switch ( nbAvas )
{
case 0:
break;
case 1:
ava = new Ava( schemaManager );
pos = ava.deserialize( buffer, pos );
avaType = ava.getNormType();
break;
default:
avas = new ArrayList<>();
avaTypes = new HashMap<>();
for ( int i = 0; i < nbAvas; i++ )
{
Ava newAva = new Ava( schemaManager );
pos = newAva.deserialize( buffer, pos );
avas.add( newAva );
List<Ava> avaList = avaTypes.get( newAva.getNormType() );
if ( avaList == null )
{
avaList = new ArrayList<>();
avaTypes.put( newAva.getNormType(), avaList );
}
avaList.add( newAva );
}
ava = null;
avaType = null;
break;
}
// Read the hashCode
h = Serialize.deserializeInt( buffer, pos );
pos += 4;
return pos;
}
/**
* A Rdn is composed of on to many Avas (AttributeType And Value).
* We should write all those Avas sequencially, following the
* structure :
* <ul>
* <li>
* <b>parentId</b> The parent entry's Id
* </li>
* <li>
* <b>nbAvas</b> The number of Avas to write. Can't be 0.
* </li>
* <li>
* <b>upName</b> The User provided Rdn
* </li>
* <li>
* <b>Avas</b>
* </li>
* </ul>
* <br>
* For each Ava :
* <ul>
* <li>
* <b>start</b> The position of this Ava in the upName string
* </li>
* <li>
* <b>length</b> The Ava user provided length
* </li>
* <li>
* <b>Call the Ava write method</b> The Ava itself
* </li>
* </ul>
*
* @see Externalizable#readExternal(ObjectInput)
* @param out The stream into which the serialized Rdn will be put
* @throws IOException If the stream can't be written
*/
@Override
public void writeExternal( ObjectOutput out ) throws IOException
{
out.writeInt( nbAvas );
out.writeUTF( upName );
switch ( nbAvas )
{
case 0:
break;
case 1:
ava.writeExternal( out );
break;
default:
for ( Ava localAva : avas )
{
localAva.writeExternal( out );
}
break;
}
out.writeInt( h );
out.flush();
}
/**
* We read back the data to create a new RDB. The structure
* read is exposed in the {@link Rdn#writeExternal(ObjectOutput)}
* method
*
* @see Externalizable#readExternal(ObjectInput)
* @param in The input stream from which the Rdn will be read
* @throws IOException If we can't read from the input stream
* @throws ClassNotFoundException If we can't create a new Rdn
*/
@Override
public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
{
StringBuilder sb = new StringBuilder();
// Read the Ava number
nbAvas = in.readInt();
// Read the UPName
upName = in.readUTF();
switch ( nbAvas )
{
case 0:
ava = null;
normName = "";
break;
case 1:
ava = new Ava( schemaManager );
ava.readExternal( in );
avaType = ava.getNormType();
buildNormRdn( sb, ava );
normName = sb.toString();
break;
default:
avas = new ArrayList<>();
avaTypes = new HashMap<>();
boolean isFirst = true;
for ( int i = 0; i < nbAvas; i++ )
{
Ava newAva = new Ava( schemaManager );
newAva.readExternal( in );
avas.add( newAva );
List<Ava> avaList = avaTypes.get( newAva.getNormType() );
if ( avaList == null )
{
avaList = new ArrayList<>();
avaTypes.put( newAva.getNormType(), avaList );
}
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( '+' );
}
buildNormRdn( sb, newAva );
avaList.add( newAva );
}
ava = null;
avaType = null;
normName = sb.toString();
break;
}
h = in.readInt();
}
private void buildNormRdn( StringBuilder sb, Ava ava )
{
sb.append( ava.getNormType() );
sb.append( '=' );
Value val = ava.getValue();
if ( ( val != null ) && ( val.getNormalized() != null ) )
{
sb.append( ava.getValue().getNormalized() );
}
}
/**
* Compare the current RDN with the provided one.
*
* @param otherRdn The RDN we want to compare to
* @return a negative value if the current RDN is below the provided one, a positive value
* if it's above and 0 if they are equal.
*/
@Override
public int compareTo( Rdn otherRdn )
{
if ( otherRdn == null )
{
return 1;
}
if ( nbAvas < otherRdn.nbAvas )
{
return -1;
}
else if ( nbAvas > otherRdn.nbAvas )
{
return 1;
}
switch ( nbAvas )
{
case 0 :
return 0;
case 1 :
int comp = ava.compareTo( otherRdn.ava );
if ( comp < 0 )
{
return -1;
}
else if ( comp > 0 )
{
return 1;
}
else
{
return 0;
}
default :
// Loop on all the Avas. We expect the Ava to be ordered
if ( isSchemaAware() )
{
return normName.compareTo( otherRdn.normName );
}
int pos = 0;
for ( Ava atav : avas )
{
Ava otherAva = otherRdn.avas.get( pos );
comp = atav.compareTo( otherAva );
if ( comp != 0 )
{
if ( comp < 0 )
{
return -1;
}
else
{
return 1;
}
}
pos++;
}
return 0;
}
}
/**
* @return a String representation of the Rdn. The caller will get back the user
* provided Rdn
*/
@Override
public String toString()
{
return upName == null ? "" : upName;
}
}