/*
 *  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.studio.ldifparser.parser;


import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.directory.studio.ldifparser.model.LdifEOFPart;
import org.apache.directory.studio.ldifparser.model.LdifEnumeration;
import org.apache.directory.studio.ldifparser.model.LdifFile;
import org.apache.directory.studio.ldifparser.model.LdifInvalidPart;
import org.apache.directory.studio.ldifparser.model.LdifPart;
import org.apache.directory.studio.ldifparser.model.container.LdifChangeAddRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifChangeDeleteRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifChangeModDnRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifChangeModifyRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifChangeRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifCommentContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifContentRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifEOFContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifInvalidContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifModSpec;
import org.apache.directory.studio.ldifparser.model.container.LdifRecord;
import org.apache.directory.studio.ldifparser.model.container.LdifSepContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifVersionContainer;
import org.apache.directory.studio.ldifparser.model.lines.LdifAttrValLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifChangeTypeLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifCommentLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifControlLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifDeloldrdnLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifDnLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifModSpecSepLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifModSpecTypeLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifNewrdnLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifNewsuperiorLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifSepLine;
import org.apache.directory.studio.ldifparser.model.lines.LdifVersionLine;


public class LdifParser
{

    private LdifScanner scanner;


    public LdifParser()
    {
        scanner = new LdifScanner();
    }


    /**
     * Parse a Ldif String. It will be stored in a LdifFile.
     * 
     * @param ldif The String to parse
     * @return The resulting LdifFile
     */
    public LdifFile parse( String ldif )
    {
        LdifFile model = new LdifFile();
        
        if ( ldif != null )
        {
            LdifEnumeration enumeration = parse( new StringReader( ldif ) );
            
            try
            {
                while ( enumeration.hasNext() )
                {
                    LdifContainer container = enumeration.next();
                    model.addContainer( container );
                }
            }
            catch ( Exception e )
            {
            }
        }
        
        return model;
    }


    public LdifEnumeration parse( Reader ldifReader )
    {
        scanner.setLdif( ldifReader );

        LdifEnumeration enumeration = new LdifEnumeration()
        {

            private List<LdifContainer> containerList = new ArrayList<LdifContainer>();

            private boolean headerParsed = false;

            private boolean bodyParsed = false;

            private boolean footerParsed = false;


            public boolean hasNext()
            {
                if ( containerList.isEmpty() )
                {
                    LdifFile model = new LdifFile();

                    // parse header
                    if ( !headerParsed )
                    {
                        checkAndParseComment( model );
                        checkAndParseVersion( model );
                        checkAndParseComment( model );
                        headerParsed = true;
                    }

                    // parse body (in a loop)
                    bodyParsed = ( headerParsed && 
                        !bodyParsed && 
                        !checkAndParseComment( model ) &&   // parse comment lines
                        !checkAndParseRecord( model ) &&    // parse record
                        !checkAndParseOther( model ) );      // parse unknown

                    // parse footer
                    if ( headerParsed && bodyParsed && !footerParsed )
                    {
                        checkAndParseComment( model );
                        footerParsed = true;
                    }

                    List<LdifContainer> containers = model.getContainers();
                    containerList.addAll( containers );
                    
                    return !containerList.isEmpty() && !( containers.get( 0 ) instanceof LdifEOFContainer );
                }
                else
                {
                    return true;
                }
            }


            public LdifContainer next()
            {
                if ( hasNext() )
                {
                    return containerList.remove( 0 );
                }
                else
                {
                    return null;
                }
            }
        };

        return enumeration;
    }


    /**
     * Checks for version line. If version line is present it is parsed and
     * added to the given model.
     * 
     * @param model
     *                the model
     * @return true if version line was added to the model, false otherwise
     */
    private boolean checkAndParseRecord( LdifFile model )
    {
        // record starts with dn-spec
        LdifToken dnSpecToken = scanner.matchDnSpec();
        
        if ( dnSpecToken == null )
        {
            return false;
        }

        // get Dn
        LdifToken dnValueTypeToken = null;
        LdifToken dnToken = null;
        LdifToken dnSepToken = null;
        dnValueTypeToken = scanner.matchValueType();
        
        if ( dnValueTypeToken != null )
        {
            dnToken = scanner.matchValue();
            
            if ( dnToken != null )
            {
                dnSepToken = scanner.matchSep();
            }
        }
        
        LdifDnLine dnLine = new LdifDnLine( dnSpecToken.getOffset(), getValueOrNull( dnSpecToken ),
            getValueOrNull( dnValueTypeToken ), getValueOrNull( dnToken ), getValueOrNull( dnSepToken ) );
        LdifToken dnErrorToken = null;
        
        if ( dnSepToken == null )
        {
            dnErrorToken = scanner.matchCleanupLine();
        }

        // save comment lines after dns
        LdifCommentLine[] commentLines = getCommentLines();

        // check record type: to decide the record type we need the next token
        // first check keywords 'control' and 'changetype'
        LdifControlLine controlLine = getControlLine();
        LdifChangeTypeLine changeTypeLine = getChangeTypeLine();
        
        if ( controlLine != null || changeTypeLine != null )
        {
            LdifChangeRecord record = null;

            // save all parts before changetype line
            List<LdifPart> partList = new ArrayList<LdifPart>();
            
            if ( dnErrorToken != null )
            {
                partList.add( new LdifInvalidPart( dnErrorToken.getOffset(), dnErrorToken.getValue() ) );
            }
            
            for ( LdifCommentLine ldifCommentLine : commentLines )
            {
                partList.add( ldifCommentLine );
            }
            
            if ( controlLine != null )
            {
                partList.add( controlLine );
                
                if ( !controlLine.isValid() )
                {
                    LdifToken errorToken = cleanupLine();
                    
                    if ( errorToken != null )
                    {
                        partList.add( new LdifInvalidPart( errorToken.getOffset(), errorToken.getValue() ) );
                    }
                }
            }

            // save comments and controls before changetype line
            while ( changeTypeLine == null && ( commentLines.length > 0 || controlLine != null ) )
            {

                commentLines = getCommentLines();
                
                for ( LdifCommentLine ldifCommentLine : commentLines )
                {
                    partList.add( ldifCommentLine );
                }

                controlLine = getControlLine();
                
                if ( controlLine != null )
                {
                    partList.add( controlLine );
                    
                    if ( !controlLine.isValid() )
                    {
                        LdifToken errorToken = cleanupLine();
                        
                        if ( errorToken != null )
                        {
                            partList.add( new LdifInvalidPart( errorToken.getOffset(), errorToken.getValue() ) );
                        }
                    }
                }

                changeTypeLine = getChangeTypeLine();
            }

            if ( changeTypeLine != null )
            {

                if ( changeTypeLine.isAdd() )
                {
                    record = new LdifChangeAddRecord( dnLine );
                    append( record, partList );
                    record.setChangeType( changeTypeLine );
                    
                    if ( !changeTypeLine.isValid() )
                    {
                        this.cleanupLine( record );
                    }
                    
                    parseAttrValRecord( record );
                }
                else if ( changeTypeLine.isDelete() )
                {
                    record = new LdifChangeDeleteRecord( dnLine );
                    append( record, partList );
                    record.setChangeType( changeTypeLine );
                    
                    if ( !changeTypeLine.isValid() )
                    {
                        this.cleanupLine( record );
                    }
                    
                    parseChangeDeleteRecord( record );
                }
                else if ( changeTypeLine.isModify() )
                {
                    record = new LdifChangeModifyRecord( dnLine );
                    append( record, partList );
                    record.setChangeType( changeTypeLine );
                    
                    if ( !changeTypeLine.isValid() )
                    {
                        this.cleanupLine( record );
                    }
                    
                    parseChangeModifyRecord( ( LdifChangeModifyRecord ) record );
                }
                else if ( changeTypeLine.isModDn() )
                {
                    record = new LdifChangeModDnRecord( dnLine );
                    append( record, partList );
                    record.setChangeType( changeTypeLine );
                    
                    if ( !changeTypeLine.isValid() )
                    {
                        this.cleanupLine( record );
                    }
                    
                    parseChangeModDnRecord( ( LdifChangeModDnRecord ) record );
                }
                else
                {
                    record = new LdifChangeRecord( dnLine );
                    append( record, partList );
                    record.setChangeType( changeTypeLine );
                    
                    if ( !changeTypeLine.isValid() )
                    {
                        this.cleanupLine( record );
                    }
                }
            }
            else
            {
                record = new LdifChangeRecord( dnLine );
                append( record, partList );
            }

            model.addContainer( record );
        }
        else
        {
            // match attr-val-record
            LdifContentRecord record = new LdifContentRecord( dnLine );
            
            if ( dnErrorToken != null )
            {
                record.addInvalid( new LdifInvalidPart( dnErrorToken.getOffset(), dnErrorToken.getValue() ) );
            }
            
            for ( LdifCommentLine ldifCommentLine : commentLines )
            {
                record.addComment( ldifCommentLine );
            }
            
            parseAttrValRecord( record );
            model.addContainer( record );
        }

        return true;
    }


    private void append( LdifChangeRecord record, List<LdifPart> partList )
    {
        for ( LdifPart ldifPart : partList )
        {
            if ( ldifPart instanceof LdifCommentLine )
            { 
                record.addComment( ( LdifCommentLine ) ldifPart );
            }
            
            if ( ldifPart instanceof LdifControlLine )
            {
                record.addControl( ( LdifControlLine ) ldifPart );
            }
            
            if ( ldifPart instanceof LdifInvalidPart )
            {
                record.addInvalid( ( LdifInvalidPart ) ldifPart );
            }
        }
    }


    private void parseChangeDeleteRecord( LdifRecord record )
    {
        do
        {
            if ( checkAndParseEndOfRecord( record ) )
            {
                return;
            }

            if ( !checkAndParseComment( record ) && !checkAndParseOther( record ) )
            {
                return;
            }
        }
        while ( true );
    }


    private void parseChangeModDnRecord( LdifChangeModDnRecord record )
    {
        boolean newrdnRead = false;
        boolean deleteoldrdnRead = false;
        boolean newsuperiorRead = false;

        do
        {
            if ( checkAndParseEndOfRecord( record ) )
            {
                return;
            }

            // comments
            checkAndParseComment( record );

            LdifToken newrdnSpecToken = null;
            LdifToken deleteoldrdnSpecToken = null;
            LdifToken newsuperiorSpecToken = null;
            
            if ( !newrdnRead )
            {
                newrdnSpecToken = scanner.matchNewrdnSpec();
            }
            
            if ( !deleteoldrdnRead && newrdnSpecToken == null )
            {
                deleteoldrdnSpecToken = scanner.matchDeleteoldrdnSpec();
            }
            
            if ( !newsuperiorRead && newrdnSpecToken == null && newsuperiorSpecToken == null )
            {
                newsuperiorSpecToken = scanner.matchNewsuperiorSpec();
            }
            

            if ( newrdnSpecToken != null )
            {
                // read newrdn line
                newrdnRead = true;
                LdifToken newrdnValueTypeToken = scanner.matchValueType();
                LdifToken newrdnValueToken = scanner.matchValue();
                LdifToken newrdnSepToken = null;
                
                if ( newrdnValueTypeToken != null || newrdnValueToken != null )
                {
                    newrdnSepToken = scanner.matchSep();
                }

                LdifNewrdnLine newrdnLine = new LdifNewrdnLine( newrdnSpecToken.getOffset(),
                    getValueOrNull( newrdnSpecToken ), getValueOrNull( newrdnValueTypeToken ),
                    getValueOrNull( newrdnValueToken ), getValueOrNull( newrdnSepToken ) );
                record.setNewrdn( newrdnLine );

                if ( newrdnSepToken == null )
                {
                    cleanupLine( record );
                }
            }
            else if ( deleteoldrdnSpecToken != null )
            {
                // read deleteoldrdnline
                deleteoldrdnRead = true;
                LdifToken deleteoldrdnValueTypeToken = scanner.matchValueType();
                LdifToken deleteoldrdnValueToken = scanner.matchValue();
                LdifToken deleteoldrdnSepToken = null;
                
                if ( deleteoldrdnValueTypeToken != null || deleteoldrdnValueToken != null )
                {
                    deleteoldrdnSepToken = scanner.matchSep();
                }

                LdifDeloldrdnLine deloldrdnLine = new LdifDeloldrdnLine( deleteoldrdnSpecToken.getOffset(),
                    getValueOrNull( deleteoldrdnSpecToken ), getValueOrNull( deleteoldrdnValueTypeToken ),
                    getValueOrNull( deleteoldrdnValueToken ), getValueOrNull( deleteoldrdnSepToken ) );
                record.setDeloldrdn( deloldrdnLine );

                if ( deleteoldrdnSepToken == null )
                {
                    cleanupLine( record );
                }
            }
            else if ( newsuperiorSpecToken != null )
            {
                // read newsuperior line
                newsuperiorRead = true;
                LdifToken newsuperiorValueTypeToken = scanner.matchValueType();
                LdifToken newsuperiorValueToken = scanner.matchValue();
                LdifToken newsuperiorSepToken = null;
                
                if ( newsuperiorValueTypeToken != null || newsuperiorValueToken != null )
                {
                    newsuperiorSepToken = scanner.matchSep();
                }

                LdifNewsuperiorLine newsuperiorLine = new LdifNewsuperiorLine( newsuperiorSpecToken.getOffset(),
                    getValueOrNull( newsuperiorSpecToken ), getValueOrNull( newsuperiorValueTypeToken ),
                    getValueOrNull( newsuperiorValueToken ), getValueOrNull( newsuperiorSepToken ) );
                record.setNewsuperior( newsuperiorLine );

                if ( newsuperiorSepToken == null )
                {
                    this.cleanupLine( record );
                }
            }
            else
            {
                if ( !checkAndParseComment( record ) && !checkAndParseOther( record ) )
                {
                    return;
                }
            }

            // comments
            checkAndParseComment( record );
        }
        while ( true );
    }


    private void parseChangeModifyRecord( LdifChangeModifyRecord record )
    {
        do
        {
            if ( checkAndParseEndOfRecord( record ) )
            {
                return;
            }

            // match mod type
            LdifToken modSpecTypeSpecToken = scanner.matchModTypeSpec();
            
            if ( modSpecTypeSpecToken != null )
            {
                // read mod type line
                LdifToken modSpecTypeValueTypeToken = null;
                LdifToken modSpecTypeAttributeDescriptionToken = null;
                LdifToken sepToken = null;
                modSpecTypeValueTypeToken = scanner.matchValueType();
                
                if ( modSpecTypeValueTypeToken != null )
                {
                    modSpecTypeAttributeDescriptionToken = scanner.matchAttributeDescription();
                    
                    if ( modSpecTypeAttributeDescriptionToken != null )
                    {
                        sepToken = scanner.matchSep();
                    }
                }
                
                LdifModSpecTypeLine modSpecTypeLine = new LdifModSpecTypeLine( modSpecTypeSpecToken.getOffset(),
                    getValueOrNull( modSpecTypeSpecToken ), getValueOrNull( modSpecTypeValueTypeToken ),
                    getValueOrNull( modSpecTypeAttributeDescriptionToken ), getValueOrNull( sepToken ) );
                LdifModSpec modSpec = new LdifModSpec( modSpecTypeLine );
                record.addModSpec( modSpec );

                // clean line
                if ( sepToken == null )
                {
                    this.cleanupLine( modSpec );
                }

                // comment
                checkAndParseComment( record );

                // read attr-val lines
                do
                {
                    LdifAttrValLine line = this.getAttrValLine();
                    
                    if ( line != null )
                    {
                        modSpec.addAttrVal( line );

                        // clean line
                        if ( "".equals( line.getRawNewLine() ) ) //$NON-NLS-1$
                        {
                            this.cleanupLine( record );
                        }
                    }
                    else
                    {
                        if ( !checkAndParseComment( record ) )
                        {
                            break;
                        }
                    }
                }
                while ( true );

                // comments
                checkAndParseComment( record );

                // read sep line
                LdifToken modSpecSepToken = scanner.matchModSep();
                
                if ( modSpecSepToken != null )
                {
                    LdifToken modSpecSepSepToken = scanner.matchSep();
                    LdifModSpecSepLine modSpecSepLine = new LdifModSpecSepLine( modSpecSepToken.getOffset(),
                        getValueOrNull( modSpecSepToken ), getValueOrNull( modSpecSepSepToken ) );
                    modSpec.finish( modSpecSepLine );
                }
            }

            if ( modSpecTypeSpecToken == null )
            {
                if ( !checkAndParseComment( record ) && !checkAndParseOther( record ) )
                {
                    return;
                }
            }
        }
        while ( true );
    }


    private void parseAttrValRecord( LdifRecord record )
    {
        do
        {
            if ( checkAndParseEndOfRecord( record ) )
            {
                return;
            }

            // check attr-val line
            LdifAttrValLine line = this.getAttrValLine();
            
            if ( line != null )
            {
                if ( record instanceof LdifContentRecord )
                {
                    ( ( LdifContentRecord ) record ).addAttrVal( line );
                }
                else if ( record instanceof LdifChangeAddRecord )
                {
                    ( ( LdifChangeAddRecord ) record ).addAttrVal( line );
                }

                // clean line
                if ( "".equals( line.getRawNewLine() ) ) //$NON-NLS-1$
                {
                    this.cleanupLine( record );
                }
            }
            else
            {
                if ( !checkAndParseComment( record ) && !checkAndParseOther( record ) )
                {
                    return;
                }
            }
        }
        while ( true );
    }


    private boolean checkAndParseEndOfRecord( LdifRecord record )
    {
        // check end of record
        LdifToken eorSepToken = scanner.matchSep();
        
        if ( eorSepToken != null )
        {
            record.finish( new LdifSepLine( eorSepToken.getOffset(), getValueOrNull( eorSepToken ) ) );
            
            return true;
        }

        // check end of file
        LdifToken eofToken = scanner.matchEOF();
        
        if ( eofToken != null )
        {
            record.finish( new LdifEOFPart( eofToken.getOffset() ) );
            return true;
        }
        
        return false;
    }


    private boolean checkAndParseComment( LdifRecord record )
    {
        LdifToken commentToken = scanner.matchComment();
        
        if ( commentToken != null )
        {
            while ( commentToken != null )
            {
                LdifToken sepToken = scanner.matchSep();
                record.addComment( new LdifCommentLine( commentToken.getOffset(), getValueOrNull( commentToken ),
                    getValueOrNull( sepToken ) ) );
                commentToken = scanner.matchComment();
            }
            
            return true;
        }
        else
        {
            return false;
        }
    }


    private boolean checkAndParseOther( LdifRecord record )
    {
        LdifToken otherToken = scanner.matchOther();
        
        if ( otherToken != null )
        {
            record.addInvalid( new LdifInvalidPart( otherToken.getOffset(), otherToken.getValue() ) );
            return true;
        }
        else
        {
            return false;
        }
    }


    /**
     * Checks for version line. If version line is present it is parsed and
     * added to the given model.
     * 
     * @param model
     *                the model
     * @return true if version line was added to the model, false otherwise
     */
    private boolean checkAndParseVersion( LdifFile model )
    {
        LdifToken versionSpecToken = scanner.matchVersionSpec();
        
        if ( versionSpecToken != null )
        {

            LdifToken versionTypeToken = null;
            LdifToken versionToken = null;
            LdifToken sepToken = null;
            versionTypeToken = scanner.matchValueType();
            
            if ( versionTypeToken != null )
            {
                versionToken = scanner.matchNumber();
                if ( versionToken != null )
                {
                    sepToken = scanner.matchSep();
                }
            }

            LdifVersionContainer container = new LdifVersionContainer( new LdifVersionLine( versionSpecToken
                .getOffset(), getValueOrNull( versionSpecToken ), getValueOrNull( versionTypeToken ),
                getValueOrNull( versionToken ), getValueOrNull( sepToken ) ) );
            model.addContainer( container );

            // clean line
            if ( sepToken == null )
            {
                this.cleanupLine( container );
            }

            return true;
        }
        else
        {
            return false;
        }
    }


    /**
     * Checks for comment lines or empty lines. If such lines are present
     * they are parsed and added to the given model.
     * 
     * @param model
     *                the model
     * @return true if comment or empty lines were added to the model, false
     *         otherwise
     */
    private boolean checkAndParseComment( LdifFile model )
    {
        LdifToken sepToken = scanner.matchSep();
        LdifToken commentToken = scanner.matchComment();

        if ( sepToken != null || commentToken != null )
        {
            while ( sepToken != null || commentToken != null )
            {
                if ( sepToken != null )
                {
                    LdifSepLine sepLine = new LdifSepLine( sepToken.getOffset(), getValueOrNull( sepToken ) );
                    LdifSepContainer sepContainer = new LdifSepContainer( sepLine );
                    model.addContainer( sepContainer );
                }

                if ( commentToken != null )
                {
                    LdifCommentContainer commentContainer = null;
                    
                    while ( commentToken != null )
                    {
                        LdifToken commentSepToken = scanner.matchSep();
                        LdifCommentLine commentLine = new LdifCommentLine( commentToken.getOffset(),
                            getValueOrNull( commentToken ), getValueOrNull( commentSepToken ) );

                        if ( commentContainer == null )
                        {
                            commentContainer = new LdifCommentContainer( commentLine );
                        }
                        else
                        {
                            commentContainer.addComment( commentLine );
                        }

                        commentToken = scanner.matchComment();
                    }
                    
                    model.addContainer( commentContainer );
                }

                sepToken = scanner.matchSep();
                commentToken = scanner.matchComment();
            }

            return true;
        }
        else
        {
            return false;
        }
    }


    /**
     * Checks for other line. If such line is present it is parsed and added
     * to the given model.
     * 
     * @param model
     *                the model
     * @return always true except if EOF reached
     */
    private boolean checkAndParseOther( LdifFile model )
    {
        LdifToken token = scanner.matchOther();
        
        if ( token != null )
        {
            LdifInvalidPart unknownLine = new LdifInvalidPart( token.getOffset(), getValueOrNull( token ) );
            LdifInvalidContainer otherContainer = new LdifInvalidContainer( unknownLine );
            model.addContainer( otherContainer );
            
            return true;
        }
        else
        {
            return false;
        }
    }


    private LdifControlLine getControlLine()
    {
        LdifToken controlSpecToken = scanner.matchControlSpec();
        
        if ( controlSpecToken != null )
        {
            LdifToken controlTypeToken = null;
            LdifToken oidToken = null;
            LdifToken criticalityToken = null;
            LdifToken valueTypeToken = null;
            LdifToken valueToken = null;
            LdifToken sepToken = null;
            controlTypeToken = scanner.matchValueType();
            
            if ( controlTypeToken != null )
            {
                oidToken = scanner.matchOid();
                if ( oidToken != null )
                {
                    criticalityToken = scanner.matchCriticality();
                    valueTypeToken = scanner.matchValueType();
                    
                    if ( valueTypeToken != null )
                    {
                        valueToken = scanner.matchValue();
                    }
                    
                    sepToken = scanner.matchSep();
                }
            }

            LdifControlLine controlLine = new LdifControlLine( controlSpecToken.getOffset(),
                getValueOrNull( controlSpecToken ), getValueOrNull( controlTypeToken ), getValueOrNull( oidToken ),
                getValueOrNull( criticalityToken ), getValueOrNull( valueTypeToken ), getValueOrNull( valueToken ),
                getValueOrNull( sepToken ) );

            return controlLine;
        }

        return null;
    }


    private LdifChangeTypeLine getChangeTypeLine()
    {
        LdifToken changeTypeSpecToken = scanner.matchChangeTypeSpec();
        
        if ( changeTypeSpecToken != null )
        {
            LdifToken changeTypeTypeToken = null;
            LdifToken changeTypeToken = null;
            LdifToken sepToken = null;
            changeTypeTypeToken = scanner.matchValueType();
            
            if ( changeTypeTypeToken != null )
            {
                changeTypeToken = scanner.matchChangeType();
                
                if ( changeTypeToken != null )
                {
                    sepToken = scanner.matchSep();
                }
            }

            LdifChangeTypeLine ctLine = new LdifChangeTypeLine( changeTypeSpecToken.getOffset(),
                getValueOrNull( changeTypeSpecToken ), getValueOrNull( changeTypeTypeToken ),
                getValueOrNull( changeTypeToken ), getValueOrNull( sepToken ) );

            return ctLine;
        }

        return null;
    }


    private LdifAttrValLine getAttrValLine()
    {
        LdifToken attrToken = scanner.matchAttributeDescription();
        
        if ( attrToken != null )
        {
            LdifToken valueTypeToken = null;
            LdifToken valueToken = null;
