/*
 *  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.valueeditors;


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.util.Strings;
import org.apache.directory.studio.ldapbrowser.common.BrowserCommonActivator;
import org.apache.directory.studio.ldapbrowser.common.BrowserCommonConstants;
import org.apache.directory.studio.ldapbrowser.core.model.AttributeHierarchy;
import org.apache.directory.studio.ldapbrowser.core.model.IAttribute;
import org.apache.directory.studio.ldapbrowser.core.model.IEntry;
import org.apache.directory.studio.ldapbrowser.core.model.IValue;
import org.apache.directory.studio.ldapbrowser.core.model.schema.Schema;
import org.apache.directory.studio.ldapbrowser.core.model.schema.SchemaUtils;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.plugin.AbstractUIPlugin;


/**
 * A ValueEditorManager is used to manage value editors. It provides methods to get
 * the best or alternative value editors for a given attribute or value. It takes
 * user preferences into account when determine the best value editor. At least
 * it provides default text and binary value editors. 
 * 
 * The available value editors are specified by the extension point
 * <code>org.apache.directory.studio.valueeditors</code>. 
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class ValueEditorManager
{
    private static final String ATTRIBUTE_TYPE = "attributeType"; //$NON-NLS-1$

    private static final String ATTRIBUTE = "attribute"; //$NON-NLS-1$

    private static final String SYNTAX_OID = "syntaxOID"; //$NON-NLS-1$

    private static final String SYNTAX = "syntax"; //$NON-NLS-1$

    private static final String ICON = "icon"; //$NON-NLS-1$

    private static final String NAME = "name"; //$NON-NLS-1$

    private static final String CLASS = "class"; //$NON-NLS-1$

    /** The extension point ID for value editors */
    private static final String EXTENSION_POINT = BrowserCommonConstants.EXTENSION_POINT_VALUE_EDITORS;

    /** The composite used to create the value editors **/
    private Composite parent;

    /** 
     * The value editor explicitly selected by the user. If this
     * member is not null it is always returned as current value editor.
     */
    private IValueEditor userSelectedValueEditor;

    /** The special value editor for multi-valued attributes */
    private MultivaluedValueEditor multiValuedValueEditor;

    /** The special value editor to edit the entry in an wizard */
    private EntryValueEditor entryValueEditor;

    /** The special value editor to rename the entry */
    private RenameValueEditor renameValueEditor;

    /** The default string editor for single-line values */
    private IValueEditor defaultStringSingleLineValueEditor;

    /** The default string editor for multi-line values */
    private IValueEditor defaultStringMultiLineValueEditor;

    /** The default binary editor */
    private IValueEditor defaultBinaryValueEditor;

    /** A map containing all available value editors. */
    private Map<String, IValueEditor> class2ValueEditors;


    /**
     * Creates a new instance of ValueEditorManager.
     *
     * @param parent the composite used to create the value editors
     */
    public ValueEditorManager( Composite parent, boolean useEntryValueEditor, boolean useRenameValueEditor )
    {
        this.parent = parent;
        userSelectedValueEditor = null;

        // init value editor map
        class2ValueEditors = new HashMap<String, IValueEditor>();
        Collection<IValueEditor> valueEditors = createValueEditors( parent );
        
        for ( IValueEditor valueEditor : valueEditors )
        {
            class2ValueEditors.put( valueEditor.getClass().getName(), valueEditor );
        }

        // special case: multivalued editor
        multiValuedValueEditor = new MultivaluedValueEditor( this.parent, this );
        multiValuedValueEditor.setValueEditorName( Messages.getString( "ValueEditorManager.MulitivaluedEditor" ) ); //$NON-NLS-1$
        multiValuedValueEditor.setValueEditorImageDescriptor( BrowserCommonActivator.getDefault().getImageDescriptor(
            BrowserCommonConstants.IMG_MULTIVALUEDEDITOR ) );

        // special case: entry editor
        if ( useEntryValueEditor )
        {
            entryValueEditor = new EntryValueEditor( this.parent, this );
            entryValueEditor.setValueEditorName( Messages.getString( "ValueEditorManager.EntryEditor" ) ); //$NON-NLS-1$
            entryValueEditor.setValueEditorImageDescriptor( BrowserCommonActivator.getDefault().getImageDescriptor(
                BrowserCommonConstants.IMG_ENTRY_EDITOR ) );
        }

        // special case: rename editor
        if ( useRenameValueEditor )
        {
            renameValueEditor = new RenameValueEditor( this.parent, this );
            renameValueEditor.setValueEditorName( Messages.getString( "ValueEditorManager.RenameEditor" ) ); //$NON-NLS-1$
            renameValueEditor.setValueEditorImageDescriptor( BrowserCommonActivator.getDefault().getImageDescriptor(
                BrowserCommonConstants.IMG_RENAME ) );
        }

        // get default editors from value editor map
        defaultStringSingleLineValueEditor = class2ValueEditors.get( InPlaceTextValueEditor.class.getName() );
        defaultStringMultiLineValueEditor = class2ValueEditors.get( TextValueEditor.class.getName() );
        defaultBinaryValueEditor = class2ValueEditors.get( HexValueEditor.class.getName() );
    }


    /**
     * Disposes all value editors.
     */
    public void dispose()
    {
        if ( parent != null )
        {
            userSelectedValueEditor = null;
            multiValuedValueEditor.dispose();
            
            if ( entryValueEditor != null )
            {
                entryValueEditor.dispose();
            }
            
            if ( renameValueEditor != null )
            {
                renameValueEditor.dispose();
            }
            
            defaultStringSingleLineValueEditor.dispose();
            defaultStringMultiLineValueEditor.dispose();
            defaultBinaryValueEditor.dispose();

            for ( IValueEditor ve : class2ValueEditors.values() )
            {
                ve.dispose();
            }

            parent = null;
        }
    }


    /**
     * Sets the value editor explicitly selected by the user. Set 
     * userSelectedValueEditor to null to remove the selection.
     *
     * @param userSelectedValueEditor the user selected value editor, may be null.
     */
    public void setUserSelectedValueEditor( IValueEditor userSelectedValueEditor )
    {
        this.userSelectedValueEditor = userSelectedValueEditor;
    }


    /**
     * Gets the value editor explicitly selected by the user.
     * 
     * @return the user selected value editor, null if non is set
     */
    public IValueEditor getUserSelectedValueEditor()
    {
        return userSelectedValueEditor;
    }


    /**
     * Returns the current (best) value editor for the given attribute.
     * 
     * <ol>
     *  <li>If a user selected value editor is selected, this is returned.
     *  <li>If a specific value editor is defined for the attribute type this 
     *      value editor is returned. See preferences. 
     *  <li>If a specific value editor is defined for the attribute's syntax this 
     *      value editor is returned. See preferences. 
     *  <li>Otherwise a default value editor is returned. If the attribute is 
     *      binary the default Hex Editor is returned. Otherwise the default 
     *      Text Editor is returned.
     * </ol>
     *
     * @param schema the schema
     * @param attributeType the attribute type
     * @return the current value editor
     */
    public IValueEditor getCurrentValueEditor( Schema schema, String attributeType )
    {
        // check user-selected (forced) value editor
        if ( userSelectedValueEditor != null )
        {
            return userSelectedValueEditor;
        }

        AttributeType atd = schema.getAttributeTypeDescription( attributeType );
        // check attribute preferences
        Map<String, String> attributeValueEditorMap = BrowserCommonActivator.getDefault().getValueEditorsPreferences()
            .getAttributeValueEditorMap();
        
        String oidStr = Strings.toLowerCase( atd.getOid() );
        
        if ( atd.getOid() != null && attributeValueEditorMap.containsKey( oidStr ) )
        {
            return ( IValueEditor ) class2ValueEditors.get( attributeValueEditorMap.get( oidStr ) );
        }
        List<String> names = atd.getNames();
        
        for ( String name : names )
        {
            String nameStr = Strings.toLowerCase( name );
            
            if ( attributeValueEditorMap.containsKey( nameStr ) )
            {
                return ( IValueEditor ) class2ValueEditors.get( attributeValueEditorMap.get( nameStr ) );
            }
        }

        // check syntax preferences
        String syntaxNumericOid = SchemaUtils.getSyntaxNumericOidTransitive( atd, schema );
        Map<String, String> syntaxValueEditorMap = BrowserCommonActivator.getDefault().getValueEditorsPreferences()
            .getSyntaxValueEditorMap();
        
        String syntaxtNumericOidStr = Strings.toLowerCase( syntaxNumericOid );
        
        if ( ( syntaxNumericOid != null ) && syntaxValueEditorMap.containsKey( syntaxtNumericOidStr ) )
        {
            return ( IValueEditor ) class2ValueEditors.get( syntaxValueEditorMap.get( syntaxtNumericOidStr ) );
        }

        // return default
        LdapSyntax lsd = schema.getLdapSyntaxDescription( syntaxNumericOid );
        
        if ( SchemaUtils.isBinary( lsd ) )
        {
            return defaultBinaryValueEditor;
        }
        else
        {
            return defaultStringSingleLineValueEditor;
        }
    }


    /**
     * Returns the current (best) value editor for the given attribute.
     * 
     * @param entry the entry
     * @param attributeType the attributge type
     * @return the current value editor
     * @see #getCurrentValueEditor( Schema, String )
     */
    public IValueEditor getCurrentValueEditor( IEntry entry, String attributeType )
    {
        return getCurrentValueEditor( entry.getBrowserConnection().getSchema(), attributeType );
    }


    /**
     * Returns the current (best) value editor for the given value.
     * 
     * @param value the value
     * @return the current value editor
     * @see #getCurrentValueEditor( Schema, String )
     */
    public IValueEditor getCurrentValueEditor( IValue value )
    {
        IAttribute attribute = value.getAttribute();
        IValueEditor ve = getCurrentValueEditor( attribute.getEntry(), attribute.getDescription() );

        // special case objectClass: always return entry editor
        if ( userSelectedValueEditor == null ) 
        {
            if ( attribute.isObjectClassAttribute() && ( entryValueEditor != null ) )
            {
                return entryValueEditor;
            }

            // special case Rdn attribute: always return rename editor
            if ( value.isRdnPart() && ( renameValueEditor != null ) )
            {
                return renameValueEditor;
            }
        }

        // here the value is known, we can check for single-line or multi-line
        if ( ve == defaultStringSingleLineValueEditor )
        {
            String stringValue = value.getStringValue();
            
            if ( ( stringValue.indexOf( '\n' ) == -1 ) && ( stringValue.indexOf( '\r' ) == -1 ) )
            {
                ve = defaultStringSingleLineValueEditor;
            }
            else
            {
                ve = defaultStringMultiLineValueEditor;
            }
        }

        return ve;
    }


    /**
     * Returns the current (best) value editor for the given attribute.
     * 
     * @param attributeHierarchy the attribute hierarchy
     * @return the current value editor
     * @see #getCurrentValueEditor( Schema, String )
     */
    public IValueEditor getCurrentValueEditor( AttributeHierarchy attributeHierarchy )
    {
        if ( attributeHierarchy == null )
        {
            return null;
        }
        else if ( ( userSelectedValueEditor == null ) && attributeHierarchy.getAttribute().isObjectClassAttribute()
            && entryValueEditor != null )
        {
            // special case objectClass: always return entry editor
            return entryValueEditor;
        }
        else if ( ( userSelectedValueEditor == entryValueEditor ) && ( entryValueEditor != null ) )
        {
            // special case objectClass: always return entry editor
            return entryValueEditor;
        }
        else if ( ( attributeHierarchy.size() == 1 ) && ( attributeHierarchy.getAttribute().getValueSize() == 0 ) )
        {
            return getCurrentValueEditor( attributeHierarchy.getAttribute().getEntry(), attributeHierarchy
                .getAttribute().getDescription() );
        }
        else if ( ( attributeHierarchy.size() == 1 ) &&
                  ( attributeHierarchy.getAttribute().getValueSize() == 1 ) &&
                  attributeHierarchy.getAttributeDescription().equalsIgnoreCase(
                      attributeHierarchy.getAttribute().getValues()[0].getAttribute().getDescription() ) )
        {
            // special case Rdn: always return MV-editor
            if ( ( userSelectedValueEditor == null ) && attributeHierarchy.getAttribute().getValues()[0].isRdnPart() )
            {
                if ( renameValueEditor != null )
                {
                    return renameValueEditor;
                }
                else
                {
                    return multiValuedValueEditor;
                }
            }

            return getCurrentValueEditor( attributeHierarchy.getAttribute().getValues()[0] );
        }
        else
        {
            return multiValuedValueEditor;
        }
    }


    /**
     * Returns alternative value editors for the given attribute. For now these
     * are the three default editors.
     *
     * @param entry the entry
     * @param attributeName the attribute
     * @return alternative value editors
     */
    public IValueEditor[] getAlternativeValueEditors( IEntry entry, String attributeName )
    {
        Schema schema = entry.getBrowserConnection().getSchema();
        
        return getAlternativeValueEditors( schema, attributeName );
    }


    /**
     * Returns alternative value editors for the given attribute. For now these
     * are the three default editors.
     * 
     * @param schema the schema
     * @param attributeName the attribute
     * @return the alternative value editors
     */
    public IValueEditor[] getAlternativeValueEditors( Schema schema, String attributeName )
    {
        List<IValueEditor> alternativeList = new ArrayList<IValueEditor>();

        AttributeType atd = schema.getAttributeTypeDescription( attributeName );

        if ( SchemaUtils.isBinary( atd, schema ) )
        {
            alternativeList.add( defaultBinaryValueEditor );
            alternativeList.add( defaultStringSingleLineValueEditor );
            alternativeList.add( defaultStringMultiLineValueEditor );
        }
        else if ( SchemaUtils.isString( atd, schema ) )
        {
            alternativeList.add( defaultStringSingleLineValueEditor );
            alternativeList.add( defaultStringMultiLineValueEditor );
            alternativeList.add( defaultBinaryValueEditor );
        }

        alternativeList.add( multiValuedValueEditor );

        alternativeList.remove( getCurrentValueEditor( schema, attributeName ) );

        return alternativeList.toArray( new IValueEditor[alternativeList.size()] );
    }


    /**
     * Returns alternative value editors for the given value. For now these
     * are the three default editors.
     *
     * @param value the value
     * @return the alternative value editors
     */
    public IValueEditor[] getAlternativeValueEditors( IValue value )
    {
        List<IValueEditor> alternativeList = new ArrayList<IValueEditor>();

        if ( value.isBinary() )
        {
            alternativeList.add( defaultBinaryValueEditor );
            alternativeList.add( defaultStringSingleLineValueEditor );
            alternativeList.add( defaultStringMultiLineValueEditor );
        }
        else if ( value.isString() )
        {
            alternativeList.add( defaultStringSingleLineValueEditor );
            alternativeList.add( defaultStringMultiLineValueEditor );
            alternativeList.add( defaultBinaryValueEditor );
        }

        alternativeList.add( multiValuedValueEditor );

        alternativeList.remove( getCurrentValueEditor( value ) );

        return alternativeList.toArray( new IValueEditor[alternativeList.size()] );
    }


    /**
     * Returns alternative value editors for the given value. For now these
     * are the three default editors.
     * 
     * @param ah the attribute hierarchy
     * @return alternative value editors
     */
    public IValueEditor[] getAlternativeValueEditors( AttributeHierarchy ah )
    {
        if ( ah == null )
        {
            return new IValueEditor[0];
        }

        // special case Rdn: no alternative to the rename editor, except the MV editor
        // perhaps this should be moved somewhere else
        if ( multiValuedValueEditor != null )
        {
            for ( IAttribute attribute : ah )
            {
                for ( IValue value : attribute.getValues() )
                {
                    if ( value.isRdnPart() )
                    {
                        return new IValueEditor[]
                            { multiValuedValueEditor };
                    }
                }
            }
        }

        // special case objectClass: no alternative to the entry editor
        // perhaps this should be moved somewhere else
        for ( IAttribute attribute : ah )
        {
            if ( attribute.isObjectClassAttribute() )
            {
                return new IValueEditor[0];
            }
        }

        if ( ( ah.size() == 1 ) && ( ah.getAttribute().getValueSize() == 0 ) )
        {
            return getAlternativeValueEditors( ah.getAttribute().getEntry(), ah.getAttribute().getDescription() );
        }
        else if ( ( ah.size() == 1 ) &&
                  ( ah.getAttribute().getValueSize() == 1 ) &&
                  ah.getAttributeDescription().equalsIgnoreCase(
                      ah.getAttribute().getValues()[0].getAttribute().getDescription() ) )
        {
            return getAlternativeValueEditors( ah.getAttribute().getValues()[0] );
        }
        else
        {
            return new IValueEditor[0];
        }
    }


    /**
     * Returns all available value editors.
     *
     * @return all available value editors
     */
    public IValueEditor[] getAllValueEditors()
    {
        // use a set to avoid double entries
        Set<IValueEditor> list = new LinkedHashSet<IValueEditor>();

        list.add( defaultStringSingleLineValueEditor );
        list.add( defaultStringMultiLineValueEditor );
        list.add( defaultBinaryValueEditor );

        list.addAll( class2ValueEditors.values() );

        list.add( multiValuedValueEditor );
        
        if ( entryValueEditor != null )
        {
            list.add( entryValueEditor );
        }
        
        if ( renameValueEditor != null )
        {
            list.add( renameValueEditor );
        }

        return list.toArray( new IValueEditor[list.size()] );
    }


    /**
     * Returns the default binary editor (a HexEditor).
     *
     * @return the default binary editor
     */
    public IValueEditor getDefaultBinaryValueEditor()
    {
        return defaultBinaryValueEditor;
    }


    /**
     * Returns the default string editor (a TextEditor).
     *
     * @return the default string editor
     */
    public IValueEditor getDefaultStringValueEditor()
    {
        return defaultStringMultiLineValueEditor;
    }


    /**
     * Returns the multi-valued editor.
     *
     * @return the multi-valued editor
     */
    public MultivaluedValueEditor getMultiValuedValueEditor()
    {
        return multiValuedValueEditor;
    }


    /**
     * Returns the entry value editor.
     *
     * @return the entry value editor
     */
    public EntryValueEditor getEntryValueEditor()
    {
        return entryValueEditor;
    }


    /**
     * Returns the rename value editor.
     *
     * @return the rename value editor
     */
    public RenameValueEditor getRenameValueEditor()
    {
        return renameValueEditor;
    }


    /**
     * Creates and returns the value editors specified by value editors extensions.
     *
     * @param parent the parent composite
     * @return the value editors
     */
    private Collection<IValueEditor> createValueEditors( Composite parent )
    {
        Collection<IValueEditor> valueEditors = new ArrayList<IValueEditor>();

        Collection<ValueEditorExtension> valueEditorExtensions = getValueEditorExtensions();
        
        for ( ValueEditorExtension vee : valueEditorExtensions )
        {
            try
            {
                IValueEditor valueEditor = ( IValueEditor ) vee.member.createExecutableExtension( CLASS );
                valueEditor.create( parent );
                valueEditor.setValueEditorName( vee.name );
                valueEditor.setValueEditorImageDescriptor( vee.icon );
                valueEditors.add( valueEditor );
            }
            catch ( Exception e )
            {
                BrowserCommonActivator.getDefault().getLog().log(
                    new Status( IStatus.ERROR, BrowserCommonConstants.PLUGIN_ID, 1, Messages
                        .getString( "ValueEditorManager.UnableToCreateValueEditor" ) //$NON-NLS-1$
                        + vee.className, e ) );
            }
        }

        return valueEditors;
    }


    /**
     * Returns all value editor extensions specified by value editor extensions.
     *
     * @return the value editor extensions
     */
    public static Collection<ValueEditorExtension> getValueEditorExtensions()
    {
        Map<String, ValueEditorExtension> valueEditorExtensions = new LinkedHashMap<>();

        IExtensionRegistry registry = Platform.getExtensionRegistry();
        IExtensionPoint extensionPoint = registry.getExtensionPoint( EXTENSION_POINT );
        IConfigurationElement[] members = extensionPoint.getConfigurationElements();

        // For each extension:
        for ( IConfigurationElement member : members )
        {
            String className = member.getAttribute( CLASS );
            String name = member.getAttribute( NAME );
            String iconPath = member.getAttribute( ICON );

            ValueEditorExtension proxy;
            if ( valueEditorExtensions.containsKey( className ) )
            {
                proxy = valueEditorExtensions.get( className );
            }
            else
            {
                proxy = new ValueEditorExtension();
                proxy.className = className;
                proxy.member = member;
                valueEditorExtensions.put( className, proxy );
            }

            if ( name != null )
            {
                proxy.name = name;
            }

            if ( iconPath != null )
            {
                IExtension extension = member.getDeclaringExtension();
                String extendingPluginId = extension.getNamespaceIdentifier();
                proxy.icon = AbstractUIPlugin.imageDescriptorFromPlugin( extendingPluginId, iconPath );
                if ( proxy.icon == null )
                {
                    proxy.icon = ImageDescriptor.getMissingImageDescriptor();
                }
            }

            IConfigurationElement[] children = member.getChildren();

            for ( IConfigurationElement child : children )
            {
                String type = child.getName();

                if ( SYNTAX.equals( type ) )
                {
                    String syntaxOID = child.getAttribute( SYNTAX_OID );
                    proxy.syntaxOids.add( syntaxOID );
                }
                else if ( ATTRIBUTE.equals( type ) )
                {
                    String attributeType = child.getAttribute( ATTRIBUTE_TYPE );
                    proxy.attributeTypes.add( attributeType );
                }
            }
        }

        return valueEditorExtensions.values();
    }

    /**
     * This class is a bean to hold the data defined in value editor extension 
     *
     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
     */
    public static class ValueEditorExtension
    {
        /** The name. */
        public String name = null;

        /** The icon. */
        public ImageDescriptor icon = null;

        /** The class name. */
        public String className = null;

        /** The syntax oids. */
        public Collection<String> syntaxOids = new ArrayList<String>( 3 );

        /** The attribute types. */
        public Collection<String> attributeTypes = new ArrayList<String>( 3 );

        /** The configuration element. */
        private IConfigurationElement member = null;
        
        
        /**
         * @see Object#toString()
         */
        public String toString()
        {
            StringBuilder sb = new StringBuilder();
            
            sb.append( '<' );
            
            sb.append( name ).append( ", " );
            sb.append(  className );
            
            if ( ( attributeTypes != null ) && ( attributeTypes.size() > 0 ) )
            {
                sb.append( ", {" );
                boolean isFirst = true;
                
                for ( String attributeType : attributeTypes )
                {
                    if ( isFirst )
                    {
                        isFirst = false;
                    }
                    else 
                    {
                        sb.append( ", " );
                    }
                    
                    sb.append( attributeType );
                }
                
                sb.append( '}' );
            }
            
            
            if ( ( syntaxOids != null ) && ( syntaxOids.size() > 0 ) )
            {
                sb.append( ", {" );
                boolean isFirst = true;
                
                for ( String syntaxOid : syntaxOids )
                {
                    if ( isFirst )
                    {
                        isFirst = false;
                    }
                    else 
                    {
                        sb.append( ", " );
                    }
                    
                    sb.append( syntaxOid );
                }
                
                sb.append( '}' );
            }

            sb.append( '>' );
            
            return sb.toString();
        }
    }
}
