blob: 721078d77a55b384cf38a046362fde880bf73167 [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.api.ldap.model.schema;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.MetaSchemaConstants;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.util.Strings;
/**
* Various utility methods for schema functions and objects.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public final class SchemaUtils
{
/**
* Private constructor.
*/
private SchemaUtils()
{
}
/**
* Gets the target entry as it would look after a modification operation
* were performed on it.
*
* @param mods the modifications performed on the entry
* @param entry the source entry that is modified
* @return the resultant entry after the modifications have taken place
* @throws LdapException if there are problems accessing attributes
*/
public static Entry getTargetEntry( List<? extends Modification> mods, Entry entry )
throws LdapException
{
Entry targetEntry = entry.clone();
for ( Modification mod : mods )
{
String id = mod.getAttribute().getId();
switch ( mod.getOperation() )
{
case REPLACE_ATTRIBUTE:
targetEntry.put( mod.getAttribute() );
break;
case ADD_ATTRIBUTE:
Attribute combined = mod.getAttribute().clone();
Attribute toBeAdded = mod.getAttribute();
Attribute existing = entry.get( id );
if ( existing != null )
{
for ( Value value : existing )
{
combined.add( value );
}
}
for ( Value value : toBeAdded )
{
combined.add( value );
}
targetEntry.put( combined );
break;
case REMOVE_ATTRIBUTE:
Attribute toBeRemoved = mod.getAttribute();
if ( toBeRemoved.size() == 0 )
{
targetEntry.removeAttributes( id );
}
else
{
existing = targetEntry.get( id );
if ( existing != null )
{
for ( Value value : toBeRemoved )
{
existing.remove( value );
}
}
}
break;
case INCREMENT_ATTRIBUTE:
// The incremented attribute might not exist
AttributeType attributeType = mod.getAttribute().getAttributeType();
String incrementStr = mod.getAttribute().getString();
int increment = 1;
if ( !Strings.isEmpty( incrementStr ) )
{
try
{
increment = Integer.parseInt( incrementStr );
}
catch ( NumberFormatException nfe )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13866_MOD_INCREMENT_INVALID_VALUE,
attributeType.getName(), incrementStr ) );
}
}
Attribute modified = targetEntry.get( attributeType );
if ( !targetEntry.containsAttribute( attributeType ) )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13867_MOD_INCREMENT_NO_ATTRIBUTE,
attributeType.getName() ) );
}
if ( !SchemaConstants.INTEGER_SYNTAX.equals( modified.getAttributeType().getSyntax().getOid() ) )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13868_MOD_INCREMENT_NO_INT_ATTRIBUTE,
attributeType.getName() ) );
}
else
{
Value[] newValues = new Value[ modified.size() ];
int i = 0;
for ( Value value : modified )
{
int intValue = Integer.parseInt( value.getNormalized() );
if ( intValue == Integer.MAX_VALUE )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_13869_MOD_INCREMENT_OVERFLOW,
attributeType.getName(), intValue ) );
}
newValues[i++] = new Value( Integer.toString( intValue + increment ) );
modified.remove( value );
}
modified.add( newValues );
}
break;
default:
throw new IllegalStateException( I18n.err( I18n.ERR_13775_UNDEFINED_MODIFICATION_TYPE, mod.getOperation() ) );
}
}
return targetEntry;
}
// ------------------------------------------------------------------------
// qdescrs rendering operations
// ------------------------------------------------------------------------
/**
* Renders qdescrs into an existing buffer.
*
* @param buf the string buffer to render the quoted description strs into
* @param qdescrs the quoted description strings to render
* @return the same string buffer that was given for call chaining
*/
public static StringBuilder render( StringBuilder buf, List<String> qdescrs )
{
if ( ( qdescrs == null ) || qdescrs.isEmpty() )
{
return buf;
}
else if ( qdescrs.size() == 1 )
{
buf.append( "'" ).append( qdescrs.get( 0 ) ).append( "'" );
}
else
{
buf.append( "( " );
for ( String qdescr : qdescrs )
{
buf.append( "'" ).append( qdescr ).append( "' " );
}
buf.append( ")" );
}
return buf;
}
/**
* Renders qdescrs into a new buffer.<br>
* <pre>
* descrs ::= qdescr | '(' WSP qdescrlist WSP ')'
* qdescrlist ::= [ qdescr ( SP qdescr )* ]
* qdescr ::= SQUOTE descr SQUOTE
* </pre>
*
* @param buf the string buffer to render the quoted description strings into
* @param qdescrs the quoted description strings to render
* @return the string buffer the qdescrs are rendered into
*/
/* No qualifier */static StringBuilder renderQDescrs( StringBuilder buf, List<String> qdescrs )
{
if ( ( qdescrs == null ) || qdescrs.isEmpty() )
{
return buf;
}
if ( qdescrs.size() == 1 )
{
buf.append( '\'' ).append( qdescrs.get( 0 ) ).append( '\'' );
}
else
{
buf.append( "( " );
for ( String qdescr : qdescrs )
{
buf.append( '\'' ).append( qdescr ).append( "' " );
}
buf.append( ")" );
}
return buf;
}
/**
* Renders QDString into a new buffer.<br>
*
* @param buf the string buffer to render the quoted description string into
* @param qdString the quoted description strings to render
* @return the string buffer the qdescrs are rendered into
*/
private static StringBuilder renderQDString( StringBuilder buf, String qdString )
{
buf.append( '\'' );
for ( char c : qdString.toCharArray() )
{
switch ( c )
{
case 0x27:
buf.append( "\\27" );
break;
case 0x5C:
buf.append( "\\5C" );
break;
default:
buf.append( c );
break;
}
}
buf.append( '\'' );
return buf;
}
// ------------------------------------------------------------------------
// objectClass list rendering operations
// ------------------------------------------------------------------------
/**
* Renders a list of object classes for things like a list of superior
* objectClasses using the ( oid $ oid ) format.
*
* @param ocs
* the objectClasses to list
* @return a buffer which contains the rendered list
*/
public static StringBuilder render( ObjectClass[] ocs )
{
StringBuilder buf = new StringBuilder();
return render( buf, ocs );
}
/**
* Renders a list of object classes for things like a list of superior
* objectClasses using the ( oid $ oid ) format into an existing buffer.
*
* @param buf
* the string buffer to render the list of objectClasses into
* @param ocs
* the objectClasses to list
* @return a buffer which contains the rendered list
*/
public static StringBuilder render( StringBuilder buf, ObjectClass[] ocs )
{
if ( ocs == null || ocs.length == 0 )
{
return buf;
}
else if ( ocs.length == 1 )
{
buf.append( ocs[0].getName() );
}
else
{
buf.append( "( " );
for ( int ii = 0; ii < ocs.length; ii++ )
{
if ( ii + 1 < ocs.length )
{
buf.append( ocs[ii].getName() ).append( " $ " );
}
else
{
buf.append( ocs[ii].getName() );
}
}
buf.append( " )" );
}
return buf;
}
// ------------------------------------------------------------------------
// attributeType list rendering operations
// ------------------------------------------------------------------------
/**
* Renders a list of attributeTypes for things like the must or may list of
* objectClasses using the ( oid $ oid ) format.
*
* @param ats
* the attributeTypes to list
* @return a buffer which contains the rendered list
*/
public static StringBuilder render( AttributeType[] ats )
{
StringBuilder buf = new StringBuilder();
return render( buf, ats );
}
/**
* Renders a list of attributeTypes for things like the must or may list of
* objectClasses using the ( oid $ oid ) format into an existing buffer.
*
* @param buf
* the string buffer to render the list of attributeTypes into
* @param ats
* the attributeTypes to list
* @return a buffer which contains the rendered list
*/
public static StringBuilder render( StringBuilder buf, AttributeType[] ats )
{
if ( ats == null || ats.length == 0 )
{
return buf;
}
else if ( ats.length == 1 )
{
buf.append( ats[0].getName() );
}
else
{
buf.append( "( " );
for ( int ii = 0; ii < ats.length; ii++ )
{
if ( ii + 1 < ats.length )
{
buf.append( ats[ii].getName() ).append( " $ " );
}
else
{
buf.append( ats[ii].getName() );
}
}
buf.append( " )" );
}
return buf;
}
// ------------------------------------------------------------------------
// schema object rendering operations
// ------------------------------------------------------------------------
/**
* Renders the schema extensions into a new StringBuffer.
*
* @param extensions the schema extensions map with key and values
* @return a StringBuffer with the extensions component of a syntax description
*/
public static StringBuilder render( Map<String, List<String>> extensions )
{
StringBuilder buf = new StringBuilder();
if ( extensions.isEmpty() )
{
return buf;
}
for ( Map.Entry<String, List<String>> entry : extensions.entrySet() )
{
buf.append( " " ).append( entry.getKey() ).append( " " );
List<String> values = entry.getValue();
// For extensions without values like X-IS-HUMAN-READIBLE
if ( values == null || values.isEmpty() )
{
continue;
}
// For extensions with a single value we can use one qdstring like 'value'
if ( values.size() == 1 )
{
buf.append( "'" ).append( values.get( 0 ) ).append( "' " );
continue;
}
// For extensions with several values we have to surround whitespace
// separated list of qdstrings like ( 'value0' 'value1' 'value2' )
buf.append( "( " );
for ( String value : values )
{
buf.append( "'" ).append( value ).append( "' " );
}
buf.append( ")" );
}
if ( buf.charAt( buf.length() - 1 ) != ' ' )
{
buf.append( " " );
}
return buf;
}
/**
* Returns a String description of a schema. The resulting String format is :
* <br>
* (OID [DESC '&lt;description&gt;'] FQCN &lt;fcqn&gt; [BYTECODE &lt;bytecode&gt;] X-SCHEMA '&lt;schema&gt;')
* <br>
* @param description The description to transform to a String
* @return The rendered schema object
*/
public static String render( LoadableSchemaObject description )
{
StringBuilder buf = new StringBuilder();
buf.append( "( " ).append( description.getOid() );
if ( description.getDescription() != null )
{
buf.append( " DESC " );
renderQDString( buf, description.getDescription() );
}
buf.append( " FQCN " ).append( description.getFqcn() );
if ( !Strings.isEmpty( description.getBytecode() ) )
{
buf.append( " BYTECODE " ).append( description.getBytecode() );
}
buf.append( " X-SCHEMA '" );
buf.append( getSchemaName( description ) );
buf.append( "' )" );
return buf.toString();
}
private static String getSchemaName( SchemaObject desc )
{
List<String> values = desc.getExtension( MetaSchemaConstants.X_SCHEMA_AT );
if ( values == null || values.isEmpty() )
{
return MetaSchemaConstants.SCHEMA_OTHER;
}
return values.get( 0 );
}
/**
* Remove the options from the attributeType, and returns the ID.
* <br>
* RFC 4512 :
* <pre>
* attributedescription = attributetype options
* attributetype = oid
* options = *( SEMI option )
* option = 1*keychar
* </pre>
*
* @param attributeId The AttributeType to parse
* @return The AttributeType without its options
*/
public static String stripOptions( String attributeId )
{
int optionsPos = attributeId.indexOf( ';' );
if ( optionsPos != -1 )
{
return attributeId.substring( 0, optionsPos );
}
else
{
return attributeId;
}
}
/**
* Get the options from the attributeType.
* <br>
* For instance, given :
* jpegphoto;binary;lang=jp
* <br>
* your get back a set containing { "binary", "lang=jp" }
*
* @param attributeId The AttributeType to parse
* @return a Set of options found for this AttributeType, or null
*/
public static Set<String> getOptions( String attributeId )
{
int optionsPos = attributeId.indexOf( ';' );
if ( optionsPos != -1 )
{
Set<String> options = new HashSet<>();
String[] res = attributeId.substring( optionsPos + 1 ).split( ";" );
for ( String option : res )
{
if ( !Strings.isEmpty( option ) )
{
options.add( option );
}
}
return options;
}
else
{
return null;
}
}
/**
* Transform an UUID in a byte array
* @param uuid The UUID to transform
* @return The byte[] representing the UUID
*/
public static byte[] uuidToBytes( UUID uuid )
{
Long low = uuid.getLeastSignificantBits();
Long high = uuid.getMostSignificantBits();
byte[] bytes = new byte[16];
bytes[0] = ( byte ) ( ( high & 0xff00000000000000L ) >> 56 );
bytes[1] = ( byte ) ( ( high & 0x00ff000000000000L ) >> 48 );
bytes[2] = ( byte ) ( ( high & 0x0000ff0000000000L ) >> 40 );
bytes[3] = ( byte ) ( ( high & 0x000000ff00000000L ) >> 32 );
bytes[4] = ( byte ) ( ( high & 0x00000000ff000000L ) >> 24 );
bytes[5] = ( byte ) ( ( high & 0x0000000000ff0000L ) >> 16 );
bytes[6] = ( byte ) ( ( high & 0x000000000000ff00L ) >> 8 );
bytes[7] = ( byte ) ( high & 0x00000000000000ffL );
bytes[8] = ( byte ) ( ( low & 0xff00000000000000L ) >> 56 );
bytes[9] = ( byte ) ( ( low & 0x00ff000000000000L ) >> 48 );
bytes[10] = ( byte ) ( ( low & 0x0000ff0000000000L ) >> 40 );
bytes[11] = ( byte ) ( ( low & 0x000000ff00000000L ) >> 32 );
bytes[12] = ( byte ) ( ( low & 0x00000000ff000000L ) >> 24 );
bytes[13] = ( byte ) ( ( low & 0x0000000000ff0000L ) >> 16 );
bytes[14] = ( byte ) ( ( low & 0x000000000000ff00L ) >> 8 );
bytes[15] = ( byte ) ( low & 0x00000000000000ffL );
return bytes;
}
/**
* Tells if an AttributeType name is valid or not. An Attribute name is valid if
* it's a descr / numericoid, as described in rfc4512 :
* <pre>
* name = descr / numericOid
* descr = keystring
* keystring = leadkeychar *keychar
* leadkeychar = ALPHA
* keychar = ALPHA / DIGIT / HYPHEN / USCORE
* numericoid = number 1*( DOT number )
* number = DIGIT / ( LDIGIT 1*DIGIT )
* ALPHA = %x41-5A / %x61-7A ; "A"-"Z" / "a"-"z"
* DIGIT = %x30 / LDIGIT ; "0"-"9"
* HYPHEN = %x2D ; hyphen ("-")
* LDIGIT = %x31-39 ; "1"-"9"
* DOT = %x2E ; period (".")
* USCORE = %x5F ; underscore ("_")
* </pre>
*
* Note that we have extended this grammar to accept the '_' char, which is widely used in teh LDAP world.
*
* @param attributeName The AttributeType name to check
* @return true if it's valid
*/
public static boolean isAttributeNameValid( String attributeName )
{
if ( Strings.isEmpty( attributeName ) )
{
return false;
}
// Check the first char which must be ALPHA or DIGIT
boolean descr;
boolean zero = false;
boolean dot = false;
char c = attributeName.charAt( 0 );
if ( ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) ) )
{
descr = true;
}
else if ( ( c >= '0' ) && ( c <= '9' ) )
{
descr = false;
zero = c == '0';
}
else
{
return false;
}
for ( int i = 1; i < attributeName.length(); i++ )
{
c = attributeName.charAt( i );
if ( descr )
{
// This is a descr, iterate on KeyChars (ALPHA / DIGIT / HYPHEN / USCORE)
if ( ( ( c < 'a' ) || ( c > 'z' ) )
&& ( ( c < 'A' ) || ( c > 'Z' ) )
&& ( ( c < '0' ) || ( c > '9' ) )
&& ( c != '-' )
&& ( c != '_' ) )
{
return false;
}
}
else
{
// This is a numericOid, check it
if ( c == '.' )
{
// Not allowed if we already have had a dot
if ( dot )
{
return false;
}
dot = true;
zero = false;
}
else if ( ( c >= '0' ) && ( c <= '9' ) )
{
dot = false;
if ( zero )
{
// We can't have a leading '0' followed by another number
return false;
}
else if ( c == '0' )
{
zero = true;
}
}
else
{
// Not valid
return false;
}
}
}
return true;
}
}