Package org.apache.harmony.jretools.policytool.control

Source Code of org.apache.harmony.jretools.policytool.control.PolicyTextParser

/*
* 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.harmony.jretools.policytool.control;

import java.util.ArrayList;
import java.util.List;

import org.apache.harmony.jretools.policytool.model.CommentEntry;
import org.apache.harmony.jretools.policytool.model.GrantEntry;
import org.apache.harmony.jretools.policytool.model.KeystoreEntry;
import org.apache.harmony.jretools.policytool.model.KeystorePasswordURLEntry;
import org.apache.harmony.jretools.policytool.model.Permission;
import org.apache.harmony.jretools.policytool.model.PolicyEntry;
import org.apache.harmony.jretools.policytool.model.Principal;

/**
* Policy text parser.
*
*/
public class PolicyTextParser {

    /** String containing the white spaces. */
    private static final String WHITE_SPACES  = " \t\n\r";

    /** Policy text to be parsed.                                                     */
    private final String              policyText;
    /** Char array of the policy text.                                                */
    private final char[]              policyTextChars;
    /** The created equivalent list of policy entries.                                */
    private final List< PolicyEntry > policyEntryList = new ArrayList< PolicyEntry >();

    /** First and last indices of the current/first token determining the next entry. */
    private int[] firstLastTokenIndices;
    /** Position of the parsing.                                                      */
    private int                       index           = 0;
    /** New position of parsing after the current entry is processed.                 */
    private int                       newIndex        = -1;

    /**
     * Creates a new PolicyTextParser.
     * @param policyText policy text to be parsed
     */
    public PolicyTextParser( final String policyText ) {
        this.policyText = policyText.replace( "\r", "" ); // I only want to handle a unified line terminator
        policyTextChars = this.policyText.toCharArray();
    }

    /**
     * Parses a policy text and returns an equivalent list of policy entries from it.
     * @param policyText policy text to be parsed
     * @return an equivalent list of policy entries created from the policy text, equivalent to policy text
     * @throws InvalidPolicyTextException thrown if policyText is invalid
     */
    public static List< PolicyEntry > parsePolicyText( final String policyText ) throws InvalidPolicyTextException {
        return new PolicyTextParser( policyText ).parsePolicyText();
    }

    /**
     * Parses the policy text and creates an equivalent list of policy entries from it.
     * @return an equivalent list of policy entries created from the policy text, equivalent to policy text
     * @throws InvalidPolicyTextException thrown if policyText is invalid
     */
    public List< PolicyEntry > parsePolicyText() throws InvalidPolicyTextException {
        while ( ( firstLastTokenIndices = peekTokenAheadCommentsIncluded( index ) ) != null ) {

            final String nextToken        = policyText.substring( firstLastTokenIndices[ 0 ], firstLastTokenIndices[ 1 ] );
            final String loweredNextToken = nextToken.toLowerCase();

            // Line comment
            if ( nextToken.startsWith( "//" ) )
                parseLineComment();

            // Block comment
            else if ( nextToken.startsWith( "/*" ) )
                parseBlockComment();

            // Keystore entry
            else if ( loweredNextToken.equals( KeystoreEntry.LOWERED_KEYWORD ) )
                parseKeystoreEntry();

            // Keystore password URL entry
            else if ( loweredNextToken.equals( KeystorePasswordURLEntry.LOWERED_KEYWORD ) )
                parseKeystorePasswordURLEntry();

            // Grant entry
            else if ( loweredNextToken.equals( GrantEntry.LOWERED_KEYWORD ) )
                parseGrantEntry();

            // Couldn't recognize any entry!
            else
                throw new InvalidPolicyTextException( "Unknown entry: " + nextToken );

            if ( newIndex >= 0 && newIndex < policyTextChars.length && policyTextChars[ newIndex ] == '\n' )
                newIndex++;

            index = newIndex;
        }

        return policyEntryList;
    }

    /**
     * Parses a line comment.
     */
    private void parseLineComment() {
        newIndex = policyText.indexOf( '\n', firstLastTokenIndices[ 0 ] + 2 );
        if ( newIndex < 0 ) // This is the last line
            newIndex = policyTextChars.length;
        policyEntryList.add( new CommentEntry( policyText.substring( index, newIndex ) ) );
    }

    /**
     * Parses a block comment.
     * @throws InvalidPolicyTextException thrown if closing bracket is missing for block comment
     */
    private void parseBlockComment() throws InvalidPolicyTextException {
        newIndex = policyText.indexOf( "*/", firstLastTokenIndices[ 0 ] + 2 );
        if ( newIndex < 0 ) // No closing bracket
            throw new InvalidPolicyTextException( "No closing bracket for block comment!" );
        newIndex += 2// length of "*/"
        policyEntryList.add( new CommentEntry( policyText.substring( index, newIndex ) ) );
    }

    /**
     * Parses a keystore entry.<br>
     * The keystoer entry syntax:
     * <pre>
     * keystore "some_keystore_url", "keystore_type", "keystore_provider";
     * </pre>
     * @throws InvalidPolicyTextException thrown if invalid keystore entry syntax is detected
     */
    private void parseKeystoreEntry() throws InvalidPolicyTextException {
        final int[] keystoreURLIndices = peekQuotedStringAhead( firstLastTokenIndices[ 1 ] );
        if ( keystoreURLIndices == null )
            throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for keystore URL!" );

        int[] keystoreTypeIndices     = null;
        int[] keystoreProviderIndices = null;

        char nextChar = peekNextNonWhiteSpaceChar( keystoreURLIndices[ 1 ] );
        if ( nextChar != PolicyEntry.TERMINATOR_CHAR ) {
            if ( nextChar != ',' )
                throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
            keystoreTypeIndices = peekQuotedStringAhead( skipWhiteSpaces( keystoreURLIndices[ 1 ] ) + 1 );
            if ( keystoreTypeIndices == null )
                throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for keystore type!" );

            nextChar = peekNextNonWhiteSpaceChar( keystoreTypeIndices[ 1 ] );
            if ( nextChar != PolicyEntry.TERMINATOR_CHAR ) {
                if ( nextChar != ',' )
                    throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
                keystoreProviderIndices = peekQuotedStringAhead( skipWhiteSpaces( keystoreTypeIndices[ 1 ] ) + 1 );
                if ( keystoreProviderIndices == null )
                    throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for keystore provider!" );

                if ( peekNextNonWhiteSpaceChar( keystoreProviderIndices[ 1 ] ) != PolicyEntry.TERMINATOR_CHAR )
                    throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
                else
                    newIndex = skipWhiteSpaces( keystoreProviderIndices[ 1 ] ) + 1;
            }
            else
                newIndex = skipWhiteSpaces( keystoreTypeIndices[ 1 ] ) + 1;
        }
        else
            newIndex = skipWhiteSpaces( keystoreURLIndices[ 1 ] ) + 1;

        final KeystoreEntry keystoreEntry = new KeystoreEntry();
        keystoreEntry.setUrl( policyText.substring( keystoreURLIndices[ 0 ] + 1, keystoreURLIndices[ 1 ] - 1 ) );
        if ( keystoreTypeIndices != null )
            keystoreEntry.setType( policyText.substring( keystoreTypeIndices[ 0 ] + 1, keystoreTypeIndices[ 1 ] - 1 ) );
        if ( keystoreProviderIndices != null )
            keystoreEntry.setProvider( policyText.substring( keystoreProviderIndices[ 0 ] + 1, keystoreProviderIndices[ 1 ] - 1 ) );
        policyEntryList.add( keystoreEntry );
    }

    /**
     * Parses a keystore password URL entry.<br>
     * The keystore password URL syntax:
     * <pre>
     * keystorePasswordURL "some_password_url";
     * </pre>
     * @throws InvalidPolicyTextException thrown if invalid keystore password URL entry syntax is detected
     */
    private void parseKeystorePasswordURLEntry() throws InvalidPolicyTextException {
        final int[] keystorePasswordURLIndices = peekQuotedStringAhead( firstLastTokenIndices[ 1 ] );
        if ( keystorePasswordURLIndices == null )
            throw new InvalidPolicyTextException( "Incomplete keystore password URL entry, found no quoted string for keystore password URL!" );

        final char nextChar = peekNextNonWhiteSpaceChar( keystorePasswordURLIndices[ 1 ] );
        if ( nextChar != PolicyEntry.TERMINATOR_CHAR )
            throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
        else
            newIndex = skipWhiteSpaces( keystorePasswordURLIndices[ 1 ] ) + 1;

        final KeystorePasswordURLEntry keystorePasswordURLEntry = new KeystorePasswordURLEntry();
        keystorePasswordURLEntry.setUrl( policyText.substring( keystorePasswordURLIndices[ 0 ] + 1, keystorePasswordURLIndices[ 1 ] - 1 ) );
        policyEntryList.add( keystorePasswordURLEntry );
    }

    /**
     * Parses a grant entry.<br>
     * The grant entry syntax:
     * <pre>
     * grant signedBy "signer_names", codeBase "URL",
     *       principal principal_class_name "principal_name",
     *       principal principal_class_name "principal_name",
     *       ... {
     *
     *     permission permission_class_name "target_name", "action",
     *         signedBy "signer_names";
     *     permission permission_class_name "target_name", "action",
     *         signedBy "signer_names";
     *     ...
     * };
     * </pre>
     * @throws InvalidPolicyTextException thrown if invalid grant entry syntax is detected
     */
    private void parseGrantEntry() throws InvalidPolicyTextException {
        final GrantEntry grantEntry = new GrantEntry();

        newIndex = firstLastTokenIndices[ 1 ];

        boolean firstIteration = true;
        char    nextChar;
        // First we parse the grant parameters such as signedBy, codeBase and the principals.
        while ( ( nextChar = peekNextNonWhiteSpaceChar( newIndex ) ) != '{' ) {

            if ( firstIteration )
                firstIteration = false;
            else
                if ( nextChar != ',' )
                    throw new InvalidPolicyTextException( "Was expecting comma but found something else!" );
                else
                    newIndex = skipWhiteSpaces( newIndex ) + 1;

            final int[] paramNameIndices = peekTokenAhead( newIndex );
            if ( paramNameIndices == null )
                throw new InvalidPolicyTextException( "Incomplete grant entry!" );
            final String loweredParamName = policyText.substring( paramNameIndices[ 0 ], paramNameIndices[ 1 ] ).toLowerCase();

            if ( loweredParamName.equals( "signedby" ) ) {
                final int[] paramValueIndices = peekQuotedStringAhead( paramNameIndices[ 1 ] );
                if ( paramValueIndices == null )
                    throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for signedBy!" );
                if ( grantEntry.getSignedBy() != null )
                    throw new InvalidPolicyTextException( "Invalid keystore entry, only 1 signedBy allowed!" );
                if ( grantEntry.getCodeBase() != null || !grantEntry.getPrincipalList().isEmpty() )
                    throw new InvalidPolicyTextException( "Invalid keystore entry, signedBy cannot be after codeBase or principals!" );
                grantEntry.setSignedBy( policyText.substring( paramValueIndices[ 0 ] + 1, paramValueIndices[ 1 ] - 1 ) );
                newIndex = paramValueIndices[ 1 ];
            }
            else if ( loweredParamName.equals( "codebase" ) ) {
                final int[] firstLastParamValueIndices = peekQuotedStringAhead( paramNameIndices[ 1 ] );
                if ( firstLastParamValueIndices == null )
                    throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for codeBase!" );
                if ( grantEntry.getCodeBase() != null )
                    throw new InvalidPolicyTextException( "Invalid keystore entry, only 1 codeBase allowed!" );
                if ( !grantEntry.getPrincipalList().isEmpty() )
                    throw new InvalidPolicyTextException( "Invalid keystore entry, signedBy cannot be after principals!" );
                grantEntry.setCodeBase( policyText.substring( firstLastParamValueIndices[ 0 ] + 1, firstLastParamValueIndices[ 1 ] - 1 ) );
                newIndex = firstLastParamValueIndices[ 1 ];
            }
            else if ( loweredParamName.equals( "principal" ) ) {
                final int[] classNameIndices = peekTokenAhead( paramNameIndices[ 1 ] );
                if ( classNameIndices == null || policyTextChars[ classNameIndices[ 0 ] ] == '{' )
                    throw new InvalidPolicyTextException( "Invalid grant entry, found no class name for principal!" );

                final int[] principalNameIndices = peekQuotedStringAhead( classNameIndices[ 1 ] );
                if ( principalNameIndices == null )
                    throw new InvalidPolicyTextException( "Invalid grant entry, found no name for principal!" );

                final Principal principal = new Principal();
                principal.setType( policyText.substring( classNameIndices[ 0 ], classNameIndices[ 1 ] ) );
                principal.setName( policyText.substring( principalNameIndices[ 0 ] + 1, principalNameIndices[ 1 ] - 1 ) );
                grantEntry.getPrincipalList().add( principal );

                newIndex = principalNameIndices[ 1 ];
            }
        }

        newIndex = policyText.indexOf( '{', newIndex ) + 1;

        // Now permissions what we have left
        while ( ( nextChar = peekNextNonWhiteSpaceChar( newIndex ) ) != '}' ) {

            final int[] keywordIndices = peekTokenAhead( newIndex );
            if ( keywordIndices == null )
                throw new InvalidPolicyTextException( "Incomplete grant entry!" );
            final String loweredKeyword = policyText.substring( keywordIndices[ 0 ], keywordIndices[ 1 ] ).toLowerCase();

            if ( !loweredKeyword.equals( "permission" ) )
                throw new InvalidPolicyTextException( "Incomplete grant entry, was expecting permission but found something else!" );

            final int[] classNameIndices = peekTokenAhead( keywordIndices[ 1 ] );
            if ( classNameIndices == null || policyTextChars[ classNameIndices[ 0 ] ] == '}' )
                throw new InvalidPolicyTextException( "Invalid grant entry, found no class name for permission!" );
            if ( classNameIndices[ 1 ] - classNameIndices[ 0 ] == 0 ) {
                if ( policyTextChars[ classNameIndices[ 0 ] ] == ',' || policyTextChars[ classNameIndices[ 0 ] ] == PolicyEntry.TERMINATOR_CHAR ) // Class name is only a comma or a semicolon
                    throw new InvalidPolicyTextException( "Invalid grant entry, found no class name for permission!" );
            }
            else {
                if ( policyTextChars[ classNameIndices[ 1 ] - 1 ] == ',' || policyTextChars[ classNameIndices[ 1 ] - ] == PolicyEntry.TERMINATOR_CHAR )
                    classNameIndices[ 1 ]--; // Class name is more than 1 character long and comma or semicolon is at its end; we step back from it
            }

            int[] targetNameIndices = null;
            int[] actionsIndices    = null;
            int[] signedByIndices   = null;

            nextChar = peekNextNonWhiteSpaceChar( classNameIndices[ 1 ] );
            if ( nextChar != PolicyEntry.TERMINATOR_CHAR ) {
                if ( nextChar != ',' ) {
                    // There must be a target name here!
                    targetNameIndices = peekQuotedStringAhead( classNameIndices[ 1 ] );
                    if ( targetNameIndices == null )
                        throw new InvalidPolicyTextException( "Invalid grant entry, found no name for permission!" );
                }

                newIndex = targetNameIndices == null ? classNameIndices[ 1 ] : targetNameIndices[ 1 ];
                nextChar = peekNextNonWhiteSpaceChar( newIndex );
                if ( nextChar != PolicyEntry.TERMINATOR_CHAR ) {
                    if ( nextChar != ',' )
                        throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );

                    newIndex = skipWhiteSpaces( newIndex );
                    nextChar = peekNextNonWhiteSpaceChar( newIndex + 1 );
                    if ( nextChar == '"' ) { // actions are specified
                        actionsIndices = peekQuotedStringAhead( newIndex + 1 );
                        if ( actionsIndices == null )
                            throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for permission actions!" );
                        newIndex = actionsIndices[ 1 ];
                    }

                    nextChar = peekNextNonWhiteSpaceChar( newIndex );
                    if ( nextChar != PolicyEntry.TERMINATOR_CHAR ) {
                        if ( nextChar != ',' )
                            throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
                        final int[] signedByKeywordIndices = peekTokenAhead( skipWhiteSpaces( newIndex ) + 1 );
                        final String loweredSignedByKeyword = policyText.substring( signedByKeywordIndices[ 0 ], signedByKeywordIndices[ 1 ] ).toLowerCase();

                        if ( !loweredSignedByKeyword.equals( "signedby" ) )
                            throw new InvalidPolicyTextException( "Incomplete grant entry, was expecting signedBy but found something else!" );

                        signedByIndices = peekQuotedStringAhead( signedByKeywordIndices[ 1 ] );
                        if ( signedByIndices == null )
                            throw new InvalidPolicyTextException( "Incomplete keystore entry, found no quoted string for permission signedBy!" );

                        if ( peekNextNonWhiteSpaceChar( signedByIndices[ 1 ] ) != PolicyEntry.TERMINATOR_CHAR )
                            throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
                        else
                            newIndex = skipWhiteSpaces( signedByIndices[ 1 ] ) + 1;
                    }
                    else
                        newIndex = skipWhiteSpaces( newIndex ) + 1;
                }
                else
                    newIndex = skipWhiteSpaces( newIndex ) + 1;
            }
            else
                newIndex = skipWhiteSpaces( classNameIndices[ 1 ] ) + 1;

            final Permission permission = new Permission();
            permission.setClassName( policyText.substring( classNameIndices[ 0 ], classNameIndices[ 1 ] ) );
            if ( targetNameIndices != null )
                permission.setTargetName( policyText.substring( targetNameIndices[ 0 ] + 1, targetNameIndices[ 1 ] - 1 ) );
            if ( actionsIndices != null )
                permission.setActions( policyText.substring( actionsIndices[ 0 ] + 1, actionsIndices[ 1 ] - 1 ) );
            if ( signedByIndices != null )
                permission.setSignedBy( policyText.substring( signedByIndices[ 0 ] + 1, signedByIndices[ 1 ] - 1 ) );
            grantEntry.getPermissionList().add( permission );
        }

        newIndex = skipWhiteSpaces( newIndex ) + 1; // index of '}' + 1 (comments skipped)
        if ( peekNextNonWhiteSpaceChar( newIndex ) != PolicyEntry.TERMINATOR_CHAR )
            throw new InvalidPolicyTextException( "Was expecting semicolon but found something else!" );
        newIndex = skipWhiteSpaces( newIndex + 1;

        policyEntryList.add( grantEntry );
    }

    /**
     * Skips the following white spaces starting from the specified index,
     * and returns the next non-white space index or -1 if end of string reached.
     * Comments are skipped over if found.
     *
     * @param index index to start from
     * @return the next non-white space index or -1 if end of string reached
     */
    private int skipWhiteSpaces( int index ) {
        try {
            boolean foundComment;
            do {
                foundComment = false;
                while ( WHITE_SPACES.indexOf( policyTextChars[ index ] ) >= 0 )
                    index++;

                if ( policyTextChars[ index ] == '/' && index < policyTextChars.length - 1 ) {
                    if ( policyTextChars[ index + 1 ] == '/' ) {
                        foundComment = true;
                        index = policyText.indexOf( '\n', index + 2 ) + 1;
                    }
                    else if ( policyTextChars[ index + 1 ] == '*' ) {
                        foundComment = true;
                        index = policyText.indexOf( "*/", index + 2 ) + 2;
                    }
                }
            } while ( foundComment ); // If foundComment, we have to recheck, because another comment might follow the the one we just found

            return index;
        } catch ( final ArrayIndexOutOfBoundsException aioobe ) {
            return -1;
        }
    }

    /**
     * Returns the first non-whitespace character starting from a given index or -1 if all the remaining characters are white spaces.
     * Comments are skipped over if found.
     * @param index index to start from
     * @return the first non-whitespace character starting from a given index or -1 if all the remaining characters are white spaces
     */
    private char peekNextNonWhiteSpaceChar( int index ) {
        index = skipWhiteSpaces( index );
        if ( index < 0 )
            return (char) -1;
        else
            return policyTextChars[ index ];
    }

    /**
     * Returns the first (inclusive) and last index (exclusive) of the next token or null if no more tokens.
     * Comments are skipped over if found.
     * @param index index to start from
     * @return the first (inclusive) and last index (exclusive) of the next token or null if no more tokens
     */
    private int[] peekTokenAhead ( int index ) {
        index = skipWhiteSpaces( index );
        if ( index < 0 )
            return null;

        final int firstIndex = index;

        while ( index++ < policyTextChars.length )
            if ( index == policyTextChars.length || WHITE_SPACES.indexOf( policyTextChars[ index ] ) >= )
                break;

        return new int[] { firstIndex, index };
    }

    /**
     * Returns the first (inclusive) and last index (exclusive) of the next quoted string or null if found something else.<br>
     * The string denoted by the returned indices includes the quotes in the beginning and in the end of the quoted string.
     * Comments are skipped over if found.
     *
     * @param index index to start from
     * @return the first (inclusive) and last index (exclusive) of the next quoted string or null if found something else
     * @throws InvalidPolicyTextException thrown if no quoted string found
     */
    private int[] peekQuotedStringAhead ( int index ) throws InvalidPolicyTextException {
        try {
            index = skipWhiteSpaces( index );
            if ( index < 0 )
                return null;

            if ( policyTextChars[ index ] != '"' )
                throw new InvalidPolicyTextException( "Could not find expected quoted string (missing opener quotation mark)!" );

            final int firstIndex = index;

            while ( ++index <= policyTextChars.length ) {
                if ( index == policyTextChars.length )
                    throw new InvalidPolicyTextException( "Could not find expected quoted string (missing closer quotation mark)!" );

                if ( policyTextChars[ index ] == '"' )
                    break;
            }

            return new int[] { firstIndex, index+1 }; // +1 because end index must be exclusive

        } catch ( final ArrayIndexOutOfBoundsException aioobe ) {
            return null;
        }
    }

    /**
     * Skips the following white spaces starting from the specified index,
     * and returns the next non-white space index or -1 if end of string reached.
     * Comments are not skipped over.
     *
     * @param index index to start from
     * @return the next non-white space index or -1 if end of string reached
     */
    private int skipWhiteSpacesCommentsIncluded( int index ) {
        try {
            while ( WHITE_SPACES.indexOf( policyTextChars[ index ] ) >= 0 )
                index++;

            return index;
        } catch ( final ArrayIndexOutOfBoundsException aioobe ) {
            return -1;
        }
    }

    /**
     * Returns the first (inclusive) and last index (exclusive) of the next token or null if no more tokens.
     * Comments are not skipped over.
     * @param index index to start from
     * @return the first (inclusive) and last index (exclusive) of the next token or null if no more tokens
     */
    private int[] peekTokenAheadCommentsIncluded( int index ) {
        index = skipWhiteSpacesCommentsIncluded( index );
        if ( index < 0 )
            return null;

        final int firstIndex = index;

        while ( index++ < policyTextChars.length )
            if ( index == policyTextChars.length || WHITE_SPACES.indexOf( policyTextChars[ index ] ) >= )
                break;

        return new int[] { firstIndex, index };
    }

}
TOP

Related Classes of org.apache.harmony.jretools.policytool.control.PolicyTextParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.