blob: 71217fc2afd258ad6d6487eb92b70b82cd6773b8 [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.ldif;
import java.io.IOException;
import javax.naming.directory.Attributes;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.AttributeUtils;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
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.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.util.Base64;
import org.apache.directory.api.util.Strings;
/**
* Some LDIF helper methods.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public final class LdifUtils
{
/** The array that will be used to match the first char.*/
private static final boolean[] LDIF_SAFE_STARTING_CHAR_ALPHABET = new boolean[128];
/** The array that will be used to match the other chars.*/
private static final boolean[] LDIF_SAFE_OTHER_CHARS_ALPHABET = new boolean[128];
/** The default length for a line in a ldif file */
private static final int DEFAULT_LINE_LENGTH = 80;
/** The file separator */
private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
static
{
// Initialization of the array that will be used to match the first char.
for ( int i = 0; i < 128; i++ )
{
LDIF_SAFE_STARTING_CHAR_ALPHABET[i] = true;
}
// 0 (NUL)
LDIF_SAFE_STARTING_CHAR_ALPHABET[0] = false;
// 10 (LF)
LDIF_SAFE_STARTING_CHAR_ALPHABET[10] = false;
// 13 (CR)
LDIF_SAFE_STARTING_CHAR_ALPHABET[13] = false;
// 32 (SPACE)
LDIF_SAFE_STARTING_CHAR_ALPHABET[32] = false;
// 58 (:)
LDIF_SAFE_STARTING_CHAR_ALPHABET[58] = false;
// 60 (>)
LDIF_SAFE_STARTING_CHAR_ALPHABET[60] = false;
// Initialization of the array that will be used to match the other chars.
for ( int i = 0; i < 128; i++ )
{
LDIF_SAFE_OTHER_CHARS_ALPHABET[i] = true;
}
// 0 (NUL)
LDIF_SAFE_OTHER_CHARS_ALPHABET[0] = false;
// 10 (LF)
LDIF_SAFE_OTHER_CHARS_ALPHABET[10] = false;
// 13 (CR)
LDIF_SAFE_OTHER_CHARS_ALPHABET[13] = false;
}
/**
* Private constructor.
*/
private LdifUtils()
{
}
/**
* Checks if the input String contains only safe values, that is, the data
* does not need to be encoded for use with LDIF. The rules for checking safety
* are based on the rules for LDIF (LDAP Data Interchange Format) per RFC 2849.
* The data does not need to be encoded if all the following are true:
*
* The data cannot start with the following char values:
* <ul>
* <li>00 (NUL)</li>
* <li>10 (LF)</li>
* <li>13 (CR)</li>
* <li>32 (SPACE)</li>
* <li>58 (:)</li>
* <li>60 (<)</li>
* <li>Any character with value greater than 127</li>
* </ul>
*
* The data cannot contain any of the following char values:
* <ul>
* <li>00 (NUL)</li>
* <li>10 (LF)</li>
* <li>13 (CR)</li>
* <li>Any character with value greater than 127</li>
* </ul>
*
* The data cannot end with a space.
*
* @param str the String to be checked
* @return true if encoding not required for LDIF
*/
public static boolean isLDIFSafe( String str )
{
if ( Strings.isEmpty( str ) )
{
// A null string is LDIF safe
return true;
}
// Checking the first char
char currentChar = str.charAt( 0 );
if ( ( currentChar > 127 ) || !LDIF_SAFE_STARTING_CHAR_ALPHABET[currentChar] )
{
return false;
}
// Checking the other chars
for ( int i = 1; i < str.length(); i++ )
{
currentChar = str.charAt( i );
if ( ( currentChar > 127 ) || !LDIF_SAFE_OTHER_CHARS_ALPHABET[currentChar] )
{
return false;
}
}
// The String cannot end with a space
return ( currentChar != ' ' );
}
/**
* Convert an Attributes as LDIF
*
* @param attrs the Attributes to convert
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attributes attrs ) throws LdapException
{
return convertAttributesToLdif( AttributeUtils.toEntry( attrs, null ), DEFAULT_LINE_LENGTH );
}
/**
* Convert an Attributes as LDIF
*
* @param attrs the Attributes to convert
* @param length The ldif line length
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attributes attrs, int length ) throws LdapException
{
return convertAttributesToLdif( AttributeUtils.toEntry( attrs, null ), length );
}
/**
* Convert an Attributes as LDIF. The Dn is written.
*
* @param attrs the Attributes to convert
* @param dn The Dn for this entry
* @param length The ldif line length
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attributes attrs, Dn dn, int length ) throws LdapException
{
return convertToLdif( AttributeUtils.toEntry( attrs, dn ), length );
}
/**
* Convert an Attributes as LDIF. The Dn is written.
*
* @param attrs the Attributes to convert
* @param dn The Dn for this entry
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attributes attrs, Dn dn ) throws LdapException
{
return convertToLdif( AttributeUtils.toEntry( attrs, dn ), DEFAULT_LINE_LENGTH );
}
/**
* Convert an Entry to LDIF
*
* @param entry the Entry to convert
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Entry entry ) throws LdapException
{
return convertToLdif( entry, DEFAULT_LINE_LENGTH );
}
/**
* Convert an Entry to LDIF including a version number at the top
*
* @param entry the Entry to convert
* @param includeVersionInfo flag to tell whether to include version number or not
* @return the corresponding LDIF code as a String
* @throws org.apache.directory.api.ldap.model.exception.LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Entry entry, boolean includeVersionInfo ) throws LdapException
{
String ldif = convertToLdif( entry, DEFAULT_LINE_LENGTH );
if ( includeVersionInfo )
{
ldif = "version: 1" + LINE_SEPARATOR + ldif;
}
return ldif;
}
/**
* Convert all the Entry's attributes to LDIF. The Dn is not written
*
* @param entry the Entry to convert
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertAttributesToLdif( Entry entry ) throws LdapException
{
return convertAttributesToLdif( entry, DEFAULT_LINE_LENGTH );
}
/**
* Convert a LDIF String to a JNDI attributes.
*
* @param ldif The LDIF string containing an attribute value
* @return An Attributes instance
* @exception LdapLdifException If the LDIF String cannot be converted to an Attributes
*/
public static Attributes getJndiAttributesFromLdif( String ldif ) throws LdapLdifException
{
LdifAttributesReader reader = new LdifAttributesReader();
try
{
Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( ldif ) );
reader.close();
return attributes;
}
catch ( IOException ioe )
{
throw new LdapLdifException( ioe.getMessage() );
}
}
/**
* Convert an Entry as LDIF
*
* @param entry the Entry to convert
* @param length the expected line length
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Entry entry, int length ) throws LdapException
{
StringBuilder sb = new StringBuilder();
if ( entry.getDn() != null )
{
// First, dump the Dn
if ( isLDIFSafe( entry.getDn().getName() ) )
{
sb.append( stripLineToNChars( "dn: " + entry.getDn().getName(), length ) );
}
else
{
sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
}
sb.append( '\n' );
}
// Then all the attributes
for ( Attribute attribute : entry )
{
sb.append( convertToLdif( attribute, length ) );
}
return sb.toString();
}
/**
* Convert the Entry's attributes to LDIF. The Dn is not written.
*
* @param entry the Entry to convert
* @param length the expected line length
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertAttributesToLdif( Entry entry, int length ) throws LdapException
{
StringBuilder sb = new StringBuilder();
// Then all the attributes
for ( Attribute attribute : entry )
{
sb.append( convertToLdif( attribute, length ) );
}
return sb.toString();
}
/**
* Convert an LdifEntry to LDIF
*
* @param entry the LdifEntry to convert
* @return the corresponding LDIF as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( LdifEntry entry ) throws LdapException
{
return convertToLdif( entry, DEFAULT_LINE_LENGTH );
}
/**
* Convert an LdifEntry to LDIF
*
* @param entry the LdifEntry to convert
* @param length The maximum line's length
* @return the corresponding LDIF as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( LdifEntry entry, int length ) throws LdapException
{
StringBuilder sb = new StringBuilder();
// First, dump the Dn
if ( isLDIFSafe( entry.getDn().getName() ) )
{
sb.append( stripLineToNChars( "dn: " + entry.getDn(), length ) );
}
else
{
sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
}
sb.append( '\n' );
// Dump the ChangeType
String changeType = Strings.toLowerCase( entry.getChangeType().toString() );
if ( entry.getChangeType() != ChangeType.None )
{
// First dump the controls if any
if ( entry.hasControls() )
{
for ( LdifControl control : entry.getControls().values() )
{
StringBuilder controlStr = new StringBuilder();
controlStr.append( "control: " ).append( control.getOid() );
controlStr.append( " " ).append( control.isCritical() );
if ( control.hasValue() )
{
controlStr.append( "::" ).append( Base64.encode( control.getValue() ) );
}
sb.append( stripLineToNChars( controlStr.toString(), length ) );
sb.append( '\n' );
}
}
sb.append( stripLineToNChars( "changetype: " + changeType, length ) );
sb.append( '\n' );
}
switch ( entry.getChangeType() )
{
case None:
if ( entry.hasControls() )
{
sb.append( stripLineToNChars( "changetype: " + ChangeType.Add, length ) );
}
// Fallthrough
case Add:
if ( ( entry.getEntry() == null ) )
{
throw new LdapException( I18n.err( I18n.ERR_12082 ) );
}
// Now, iterate through all the attributes
for ( Attribute attribute : entry.getEntry() )
{
sb.append( convertToLdif( attribute, length ) );
}
break;
case Delete:
if ( entry.getEntry() != null )
{
throw new LdapException( I18n.err( I18n.ERR_12081 ) );
}
break;
case ModDn:
case ModRdn:
if ( entry.getEntry() != null )
{
throw new LdapException( I18n.err( I18n.ERR_12083 ) );
}
// Stores the new Rdn
Attribute newRdn = new DefaultAttribute( "newrdn", entry.getNewRdn() );
sb.append( convertToLdif( newRdn, length ) );
// Stores the deleteoldrdn flag
sb.append( "deleteoldrdn: " );
if ( entry.isDeleteOldRdn() )
{
sb.append( "1" );
}
else
{
sb.append( "0" );
}
sb.append( '\n' );
// Stores the optional newSuperior
if ( !Strings.isEmpty( entry.getNewSuperior() ) )
{
Attribute newSuperior = new DefaultAttribute( "newsuperior", entry.getNewSuperior() );
sb.append( convertToLdif( newSuperior, length ) );
}
break;
case Modify:
boolean isFirst = true;
for ( Modification modification : entry.getModifications() )
{
if ( isFirst )
{
isFirst = false;
}
else
{
sb.append( "-\n" );
}
switch ( modification.getOperation() )
{
case ADD_ATTRIBUTE:
sb.append( "add: " );
break;
case REMOVE_ATTRIBUTE:
sb.append( "delete: " );
break;
case REPLACE_ATTRIBUTE:
sb.append( "replace: " );
break;
default:
throw new IllegalArgumentException( "Unexpected ModificationOperation: "
+ modification.getOperation() );
}
sb.append( modification.getAttribute().getUpId() );
sb.append( '\n' );
sb.append( convertToLdif( modification.getAttribute(), length ) );
}
sb.append( '-' );
break;
default:
throw new IllegalArgumentException( "Unexpected ChangeType: " + entry.getChangeType() );
}
sb.append( '\n' );
return sb.toString();
}
/**
* Base64 encode a String
*
* @param str The string to encode
* @return the base 64 encoded string
*/
private static String encodeBase64( String str )
{
// force encoding using UTF-8 charset, as required in RFC2849 note 7
return new String( Base64.encode( Strings.getBytesUtf8( str ) ) );
}
/**
* Converts an EntryAttribute to LDIF
*
* @param attr the >EntryAttribute to convert
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attribute attr ) throws LdapException
{
return convertToLdif( attr, DEFAULT_LINE_LENGTH );
}
/**
* Converts an EntryAttribute as LDIF
*
* @param attr the EntryAttribute to convert
* @param length the expected line length
* @return the corresponding LDIF code as a String
* @throws LdapException If a naming exception is encountered.
*/
public static String convertToLdif( Attribute attr, int length ) throws LdapException
{
StringBuilder sb = new StringBuilder();
if ( attr.size() == 0 )
{
// Special case : we don't have any value
return "";
}
for ( Value<?> value : attr )
{
StringBuilder lineBuffer = new StringBuilder();
lineBuffer.append( attr.getUpId() );
// First, deal with null value (which is valid)
if ( value.isNull() )
{
lineBuffer.append( ':' );
}
else if ( value.isHumanReadable() )
{
// It's a String but, we have to check if encoding isn't required
String str = value.getString();
if ( !LdifUtils.isLDIFSafe( str ) )
{
lineBuffer.append( ":: " ).append( encodeBase64( str ) );
}
else
{
lineBuffer.append( ':' );
if ( str != null )
{
lineBuffer.append( ' ' ).append( str );
}
}
}
else
{
// It is binary, so we have to encode it using Base64 before adding it
char[] encoded = Base64.encode( value.getBytes() );
lineBuffer.append( ":: " + new String( encoded ) );
}
lineBuffer.append( '\n' );
sb.append( stripLineToNChars( lineBuffer.toString(), length ) );
}
return sb.toString();
}
/**
* Strips the String every n specified characters
*
* @param str the string to strip
* @param nbChars the number of characters
* @return the stripped String
*/
public static String stripLineToNChars( String str, int nbChars )
{
int strLength = str.length();
if ( strLength <= nbChars )
{
return str;
}
if ( nbChars < 2 )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_12084 ) );
}
// We will first compute the new size of the LDIF result
// It's at least nbChars chars plus one for \n
int charsPerLine = nbChars - 1;
int remaining = ( strLength - nbChars ) % charsPerLine;
int nbLines = 1 + ( ( strLength - nbChars ) / charsPerLine ) + ( remaining == 0 ? 0 : 1 );
int nbCharsTotal = strLength + nbLines + nbLines - 2;
char[] buffer = new char[nbCharsTotal];
char[] orig = str.toCharArray();
int posSrc = 0;
int posDst = 0;
System.arraycopy( orig, posSrc, buffer, posDst, nbChars );
posSrc += nbChars;
posDst += nbChars;
for ( int i = 0; i < nbLines - 2; i++ )
{
buffer[posDst++] = '\n';
buffer[posDst++] = ' ';
System.arraycopy( orig, posSrc, buffer, posDst, charsPerLine );
posSrc += charsPerLine;
posDst += charsPerLine;
}
buffer[posDst++] = '\n';
buffer[posDst++] = ' ';
System.arraycopy( orig, posSrc, buffer, posDst, remaining == 0 ? charsPerLine : remaining );
return new String( buffer );
}
/**
* Build a new Attributes instance from a LDIF list of lines. The values can be
* either a complete Ava, or a couple of AttributeType ID and a value (a String or
* a byte[]). The following sample shows the three cases :
*
* <pre>
* Attribute attr = AttributeUtils.createAttributes(
* "objectclass: top",
* "cn", "My name",
* "jpegPhoto", new byte[]{0x01, 0x02} );
* </pre>
*
* @param avas The AttributeType and Values, using a ldif format, or a couple of
* Attribute ID/Value
* @return An Attributes instance
* @throws LdapException If the data are invalid
*/
public static Attributes createJndiAttributes( Object... avas ) throws LdapException
{
StringBuilder sb = new StringBuilder();
int pos = 0;
boolean valueExpected = false;
for ( Object ava : avas )
{
if ( !valueExpected )
{
if ( !( ava instanceof String ) )
{
throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
I18n.ERR_12085, ( pos + 1 ) ) );
}
String attribute = ( String ) ava;
sb.append( attribute );
if ( attribute.indexOf( ':' ) != -1 )
{
sb.append( '\n' );
}
else
{
valueExpected = true;
}
}
else
{
if ( ava instanceof String )
{
sb.append( ": " ).append( ( String ) ava ).append( '\n' );
}
else if ( ava instanceof byte[] )
{
sb.append( ":: " );
sb.append( new String( Base64.encode( ( byte[] ) ava ) ) );
sb.append( '\n' );
}
else
{
throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
I18n.ERR_12086, ( pos + 1 ) ) );
}
valueExpected = false;
}
}
if ( valueExpected )
{
throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n
.err( I18n.ERR_12087 ) );
}
LdifAttributesReader reader = new LdifAttributesReader();
Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( sb.toString() ) );
try
{
reader.close();
}
catch ( IOException e )
{
e.printStackTrace();
}
return attributes;
}
}