/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.openquark.cal.services;
import java.util.ArrayList;
import java.util.List;
import org.openquark.cal.compiler.LanguageInfo;
/**
* Provides helper methods for creating valid CAL identifiers, and for describing the problems in proposed CAL
* identifiers. This class is similar to LanguageInfo (and in fact was split off from it).
*
* @author Ulian Radu
*/
public final class IdentifierUtils {
/**
* Class handling validation of CAL identifiers.
*
* @author Iulian Radu
*/
private static class IdentifierValidator {
/** Identifier which we are validating */
private final String identifier;
/** Position we are currently validating within the identifier */
private int identifierIndex;
/** Buffer building up a valid suggestion */
private final StringBuilder suggestionBuffer;
/**
* Result of this validation. While validation is in progress,
* this structure gathers validation errors.
*/
private final ValidatedIdentifier validationResult;
/**
* Flag indicating case restrictions on the identifier.
* (if true, identifier needs to start with an upper case character.)
*/
private final boolean isConstructor;
/**
* Constructs object and initializes fields.
*
* @param identifier string to convert from
* @param isConstructor true if identifier should be a constructor; false otherwise
*/
private IdentifierValidator(String identifier, boolean isConstructor) {
validationResult = new ValidatedIdentifier();
this.isConstructor = isConstructor;
this.identifier = identifier;
identifierIndex = 0;
suggestionBuffer = new StringBuilder(identifier.length());
}
/**
* Make a valid CAL identifier name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* identifier name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* If isConstructor is true, the argument needs to have a capital letter at its
* beginning in order to be valid (use this for Constructors or Data Type names).
*
* @return a structure for the validated identifier name (containing validation
* errors and its valid suggestion)
*/
private ValidatedIdentifier makeIdentifierName()
{
// Exit if identifier is null
if (identifier == null || identifier.length() == 0) {
validationResult.addError(ValidationStatus.WAS_EMPTY);
return validationResult;
}
// Validate beginning and content of identifier
validateIdentifierBegining();
if (suggestionBuffer.length() == 0) {
// All the characters are invalid
return validationResult;
}
validateIdentifierContent();
// Now ensure identifier is not a keyword
String newIdentifier = suggestionBuffer.toString();
if (LanguageInfo.isKeyword(newIdentifier)) {
validationResult.addError(ValidationStatus.EXISTING_KEYWORD);
int i = 0;
while (LanguageInfo.isKeyword(newIdentifier + i)) {
i++;
}
newIdentifier = newIdentifier + i;
}
// Return result and suggestion (if necessary)
if (validationResult.getNErrors() > 0) {
validationResult.setSuggestion(newIdentifier);
}
return validationResult;
}
/**
* Validates the beginning of an identifier by stripping any illegal characters from
* the string. Errors are added into validationResult, and the
* suggestion buffer will contain the first validated character. Identifier index
* will point to the character after the first valid character.
*
* NOTE: This method should only be called via makeIdentifierName(), as it
* modifies the existing validation result.
*/
private void validateIdentifierBegining() {
// keep consuming the input string until we can find the first character
// that is a valid starting character for a CAL lexer token
while (identifierIndex < identifier.length()) {
// Try to correct the first letter
char test = identifier.charAt(identifierIndex);
ValidationStatus e = (isConstructor ? checkCALConsStart(test) : checkCALVarStart(test));
char c = makeValidCharacter(test,e);
// Should we use this correction ?
if (e != ValidationStatus.INVALID_CONTENT) {
// Character has been corrected and can be added
suggestionBuffer.append(c);
if (e.isError()) {
validationResult.addError(e);
}
break;
} else {
// Letter is not and could not be corrected; log error and discard it
if (identifierIndex == 0) {
validationResult.addError(ValidationStatus.INVALID_START);
}
identifierIndex++;
}
}
identifierIndex++;
}
/**
* Validates the content of an identifier by stripping any illegal
* characters from the string. Errors are added into validationResult,
* and the result string builder will contain a validated identifier.
*
* NOTE: This method should only be called via makeIdentifierName(), as it
* modifies the existing validation result.
*/
private void validateIdentifierContent() {
// Will only store invalid content error once
boolean indicatedBadContent = false;
// Flag indicates that the preceding letter was invalid, so the next valid letter must be upper cased.
// Never true if inCapsStart is true.
boolean camelCase = false;
// If the identifier starts with a string of upper-case letters, these are all lower-cased,
// except for the last letter if it precedes a lower-case letter.
// This flag is true until a non-upper case letter is encountered.
// eg. FOOBar -> fooBar
boolean inCapsStart = LanguageInfo.isCALConsStart(identifier.charAt(identifierIndex - 1)); // never true if camelCase is true
for (int size = identifier.length(); identifierIndex < size; identifierIndex++) {
char test = identifier.charAt(identifierIndex);
ValidationStatus e = (isConstructor ? checkCALConsPart(test) : checkCALVarPart(test));
char c = makeValidCharacter(test,e);
if (e.isError() && !indicatedBadContent) {
validationResult.addError(e);
indicatedBadContent = true;
}
if (e == ValidationStatus.INVALID_CONTENT) {
// Strip invalid content and replace next valid letter by upper case
camelCase = true;
inCapsStart = false;
// If we encountered a space char, suggest underscore as a replacement instead of just stripping the character.
if (Character.isSpaceChar(test)) {
suggestionBuffer.append('_');
}
} else {
if (camelCase) {
// The previous letter was invalid.
c = Character.toUpperCase(c);
camelCase = false;
} else if (inCapsStart) {
if (LanguageInfo.isCALConsStart(test)) {
// This letter is upper case, and all preceding letters in the identifier were also upper case.
if (identifierIndex + 1 >= size || LanguageInfo.isCALConsStart(identifier.charAt(identifierIndex + 1))) {
// This is the last letter, or the next letter is also upper case.
c = Character.toLowerCase(c);
} else {
// There's another letter, and it isn't upper case.
inCapsStart = false;
}
} else {
// This letter isn't upper case.
inCapsStart = false;
}
}
suggestionBuffer.append(c);
}
}
}
}
/**
* Class enumerating status of an identifier validation.
*
* @author Iulian Radu
*/
public static final class ValidationStatus {
private final String enumType;
private ValidationStatus(String enumType) {
if (enumType == null) {
throw new NullPointerException();
}
this.enumType = enumType;
}
/**
* @return true if the validation returned an error
*/
public boolean isError() {
return (this != ValidationStatus.NO_ERROR);
}
/**
* Enumeration indicating that no error has occurred
*/
private static final ValidationStatus NO_ERROR = new ValidationStatus("NO_ERROR");
/**
* Error indicating that specified identifier was empty
*/
public static final ValidationStatus WAS_EMPTY = new ValidationStatus("WAS_EMPTY");
/**
* Error indicating illegal characters at the start of identifier
*/
public static final ValidationStatus INVALID_START = new ValidationStatus("INVALID_START");
/**
* Error indicating that the identifier is already a keyword
*/
public static final ValidationStatus EXISTING_KEYWORD = new ValidationStatus("EXISTING_KEYWORD");
/**
* Error indicating that a character needs to be upper case
*/
public static final ValidationStatus NEED_UPPER = new ValidationStatus("NEED_UPPER");
/**
* Error indicating that a character needs to be lower case
*/
public static final ValidationStatus NEED_LOWER = new ValidationStatus("NEED_LOWER");
/**
* Error indicating that a character is invalid
*/
public static final ValidationStatus INVALID_CONTENT = new ValidationStatus("INVALID_CONTENT");
/**
* Converts enumeration to string.
*/
@Override
public String toString() {
return enumType;
}
}
/**
* Class used for holding result of validation operations.
* This holds an indicator whether a validation was successful,
* the suggested correction (if any), and a list of errors encountered.
*
* @author Iulian Radu
*/
public static class ValidatedIdentifier {
/**
* Indicates whether specified validation succeeded
*/
private boolean valid;
/**
* Holds suggested valid identifier value.
* If no suggestion could be created, this is null.
*/
private String suggestion;
/**
* The list of ValidationStatus objects detailing the
* errors encountered. The errors are stored in
* the order they were added.
*/
private List<ValidationStatus> errors = new ArrayList<ValidationStatus>();
/**
* Constructor
*/
private ValidatedIdentifier() {
valid = true;
suggestion = null;
errors.clear();
}
/**
* @return suggested value
*/
public String getSuggestion() {
return suggestion;
}
/**
* Sets the suggested value as specified
* @param newSuggestion
*/
private void setSuggestion(String newSuggestion) {
suggestion = newSuggestion;
}
/**
* @return true if suggestion exists; false if not
*/
public boolean hasSuggestion() {
return suggestion!=null;
}
/**
* Retrieves the specified error
*
* @param index error number
* @return specified error
*/
public ValidationStatus getNthError(int index) {
return errors.get(index);
}
/**
* Adds specified error to the existing error list
*
* @param error error to add
*/
private void addError(ValidationStatus error) {
if (error.isError()) {
setValid(false);
errors.add(error);
}
}
/**
* Retrieves the number of errors encountered
*
* @return number of errors
*/
public int getNErrors() {
return errors.size();
}
/**
* @return true if validation was successful; false if not
*/
public boolean isValid() {
return valid;
}
/**
* Sets the value of valid field.
* @param newValid
*/
private void setValid(boolean newValid) {
valid = newValid;
}
}
/**
* Make a valid CAL module name component, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* module name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated module name component (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidModuleNameComponent(String identifier) {
return makeValidatedIdentifier(identifier, true);
}
/**
* Make a valid CAL module name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* module name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated module name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidModuleName(String identifier) {
String[] components = identifier.split("\\.");
ValidatedIdentifier result = new ValidatedIdentifier();
StringBuilder finalSuggestion = new StringBuilder();
for (int i = 0; i < components.length; i++) {
ValidatedIdentifier validatedComponent = new IdentifierValidator(components[i], true).makeIdentifierName();
if (validatedComponent.hasSuggestion()) {
if (i > 0) {
finalSuggestion.append('.');
}
finalSuggestion.append(validatedComponent.getSuggestion());
}
// if the validated component has no suggestion, we simply skip the component
if (!validatedComponent.isValid()) {
result.setValid(false);
}
for (int j = 0; j < validatedComponent.getNErrors(); j++) {
result.addError(validatedComponent.getNthError(j));
}
}
if (finalSuggestion.length() > 0) {
result.setSuggestion(finalSuggestion.toString());
}
return result;
}
/**
* Make a valid CAL function name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* function name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated function name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidFunctionName(String identifier) {
return makeValidatedIdentifier(identifier, false);
}
/**
* Make a valid CAL type constructor name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* type constructor name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated type constructor name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidTypeConstructorName(String identifier) {
return makeValidatedIdentifier(identifier, true);
}
/**
* Make a valid CAL type variable name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* type variable name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated type variable name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidTypeVariableName(String identifier) {
return makeValidatedIdentifier(identifier, false);
}
/**
* Make a valid CAL data constructor name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* data constructor name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated data constructor name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidDataConstructorName(String identifier) {
return makeValidatedIdentifier(identifier, true);
}
/**
* Make a valid CAL type class name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* type class name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated type class name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidTypeClassName(String identifier) {
return makeValidatedIdentifier(identifier, true);
}
/**
* Make a valid CAL class method name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* class method name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* @param identifier the string to convert from
* @return a structure for the validated class method name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidClassMethodName(String identifier) {
return makeValidatedIdentifier(identifier, false);
}
/**
* Make a valid CAL identifier name from an arbitrary string. This method removes all
* characters which are invalid, except for space characters, which may be replaced with underscores.
* If the given string only contains invalid characters, then <code>null</code> is returned.
* If the output string is in fact a CAL keyword, then this method appends a number
* pad at the end of the identifier.
*
* @param nameToConvert the string to convert from
* @param isConstructor True for a constructor, false for a var.
*
* @return the valid identifier name
*/
public static String makeIdentifierName(String nameToConvert, boolean isConstructor) {
ValidatedIdentifier validatedIdentifier = makeValidatedIdentifier(nameToConvert, isConstructor);
return validatedIdentifier.isValid() ? nameToConvert : validatedIdentifier.getSuggestion();
}
/**
* Make a valid CAL identifier name from an arbitrary string. This method removes
* all characters which are invalid and it replaces all space characters with
* underscores. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method simply append a static text string "_0" at the end of
* the output.
*
* @param identifier the string to convert from
* @return the valid identifier name
*/
public static String makeIdentifierName(String identifier) {
return makeIdentifierName(identifier, false);
}
/**
* Checks that the specified character is valid for
* the start of a CAL constructor identifier, and returns an error if not.
*
* @param c character to convert
* @return NO_ERROR if no error;
* NEED_UPPER if character needs to be upper cased
* INVALID_CONTENT if character cannot be corrected by changing case
*/
static private ValidationStatus checkCALConsStart(char c) {
if (LanguageInfo.isCALConsStart(c)) {
return ValidationStatus.NO_ERROR;
} else if (c >= 'a' && c <= 'z') {
return ValidationStatus.NEED_UPPER;
} else {
return ValidationStatus.INVALID_CONTENT;
}
}
/**
* Checks that the specified character is valid for
* the content of a CAL constructor, and returns an error if not.
*
* @param c character to convert
* @return NO_ERROR if no error;
* INVALID_CONTENT if character cannot be corrected by changing case
*/
static private ValidationStatus checkCALConsPart(char c) {
return checkCALVarPart(c);
}
/**
* Checks that the specified character is valid for
* the start of a CAL variable identifier, and returns an error if not.
*
* @param c character to convert
* @return NO_ERROR if no error;
* NEED_LOWER if character needs to be lower cased
* INVALID_CONTENT if character cannot be corrected by a case change
*/
static private ValidationStatus checkCALVarStart(char c) {
if (LanguageInfo.isCALVarStart(c)) {
return ValidationStatus.NO_ERROR;
} else if (c >= 'A' && c <= 'Z') {
return ValidationStatus.NEED_LOWER;
} else {
return ValidationStatus.INVALID_CONTENT;
}
}
/**
* Checks that the specified character is valid for
* the start of a CAL constructor, and returns an error if not.
*
* @param c character to convert
* @return NO_ERROR if no error;
* INVALID_CONTENT if character cannot be corrected by case change
*/
static private ValidationStatus checkCALVarPart(char c) {
if (LanguageInfo.isCALVarPart(c)) {
return ValidationStatus.NO_ERROR;
} else {
return ValidationStatus.INVALID_CONTENT;
}
}
/**
* Converts the specified character into a valid identifier
* character by taking into account the validation error.
*
* @param c character to be corrected
* @param e error from validating the character
* @return valid character or \0 if unknown error
*/
static private char makeValidCharacter(char c, ValidationStatus e) {
if (e == ValidationStatus.NO_ERROR) {
return c;
} else if (e == ValidationStatus.NEED_UPPER) {
return Character.toUpperCase(c);
} else if (e == ValidationStatus.NEED_LOWER) {
return Character.toLowerCase(c);
} else if (e == ValidationStatus.INVALID_CONTENT) {
return '_';
} else if (e == null) {
throw new NullPointerException();
} else {
throw new IllegalArgumentException();
}
}
/**
* Make a valid CAL identifier name, and indicate the corrections made. This method
* removes all characters which are invalid and ensures camel casing on the resulting
* identifier name. If the given string only contains invalid characters, then
* <code>null</code> is returned. If the output string is in fact a CAL keyword,
* then this method appends a number pad at the end of the identifier.
*
* If isConstructor is true, the argument needs to have a capital letter at its
* beginning in order to be valid (use this for Constructors or Data Type names).
*
* @param identifier the string to convert from
* @param isConstructor true if identifier should be a constructor; false otherwise
* @return a structure for the validated identifier name (containing validation
* errors and its valid suggestion)
*/
public static ValidatedIdentifier makeValidatedIdentifier(
String identifier,
boolean isConstructor) {
IdentifierValidator validator = new IdentifierValidator(identifier, isConstructor);
return validator.makeIdentifierName();
}
}