blob: ba0c0388ff235bfcfc316e88082ef217757b49ec [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package org.apache.sling.commons.compiler.source;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;
/**
* The {@code JavaEscapeHelper} class provides various methods which can be used to generate valid Java identifiers or package / class
* names, especially when generating them from a transcompiler.
*/
public final class JavaEscapeHelper {
private static final Set<String> javaKeywords = new HashSet<>();
private static final Set<String> literals = new HashSet<>();
private static final Set<String> specialIdentifiers = new HashSet<>();
private static final Pattern ESCAPED_CHAR_PATTERN = Pattern.compile("(__[0-9a-f]{4}__)");
private JavaEscapeHelper() {
}
static {
javaKeywords.add("abstract");
javaKeywords.add("assert");
javaKeywords.add("boolean");
javaKeywords.add("break");
javaKeywords.add("byte");
javaKeywords.add("case");
javaKeywords.add("catch");
javaKeywords.add("char");
javaKeywords.add("class");
javaKeywords.add("const");
javaKeywords.add("continue");
javaKeywords.add("default");
javaKeywords.add("do");
javaKeywords.add("double");
javaKeywords.add("else");
javaKeywords.add("enum");
javaKeywords.add("extends");
javaKeywords.add("final");
javaKeywords.add("finally");
javaKeywords.add("float");
javaKeywords.add("for");
javaKeywords.add("goto");
javaKeywords.add("if");
javaKeywords.add("implements");
javaKeywords.add("import");
javaKeywords.add("instanceof");
javaKeywords.add("int");
javaKeywords.add("interface");
javaKeywords.add("long");
javaKeywords.add("native");
javaKeywords.add("new");
javaKeywords.add("package");
javaKeywords.add("private");
javaKeywords.add("protected");
javaKeywords.add("public");
javaKeywords.add("return");
javaKeywords.add("short");
javaKeywords.add("static");
javaKeywords.add("strictfp");
javaKeywords.add("super");
javaKeywords.add("switch");
javaKeywords.add("synchronized");
javaKeywords.add("this");
javaKeywords.add("throw");
javaKeywords.add("throws");
javaKeywords.add("transient");
javaKeywords.add("try");
javaKeywords.add("void");
javaKeywords.add("volatile");
javaKeywords.add("while");
javaKeywords.add("_");
literals.add("true");
literals.add("false");
literals.add("null");
specialIdentifiers.add("var");
}
/**
* Converts the given identifier to a legal Java identifier.
*
* @param identifier the identifier to convert
* @return legal Java identifier corresponding to the given identifier
*/
public static @NotNull String getJavaIdentifier(@NotNull String identifier) {
StringBuilder modifiedIdentifier = new StringBuilder();
char[] identifierChars = new char[identifier.length()];
identifier.getChars(0, identifier.length(), identifierChars, 0);
for (int i = 0; i < identifierChars.length; i++) {
char ch = identifierChars[i];
if (i == 0 && !Character.isJavaIdentifierStart(ch)) {
modifiedIdentifier.append(escapeChar(ch));
} else {
if (!Character.isJavaIdentifierPart(ch)) {
modifiedIdentifier.append(escapeChar(ch));
} else {
modifiedIdentifier.append(ch);
}
}
}
String currentIdentifier = modifiedIdentifier.toString();
if (isJavaKeyword(currentIdentifier) || isJavaLiteral(currentIdentifier) || isSpecialIdentifier(currentIdentifier)) {
return escapeChar(currentIdentifier.charAt(0)) + currentIdentifier.substring(1);
}
return currentIdentifier;
}
/**
* Escapes the provided character so that it's a valid Java identifier character. This method does not check if the provided
* character is valid and will escape any character into a sequence matching the {@code (__[0-9a-f]{4}__)} pattern, by using the
* character's decimal code and converting it to hexadecimal.
*
* @param ch the character to escape
* @return the escaped character representation
*/
public static @NotNull String escapeChar(char ch) {
return String.format("__%04x__", (int) ch);
}
/**
* Returns the original character that was escaped, given an {@code escapeSequence}. The {@code escapeSequence} has to match the
* {@code (__[0-9a-f]{4}__)} pattern.
*
* @param escapeSequence the escaped string
* @return the original character
* @throws IllegalArgumentException if the {@code escaped} string does not match the (__[0-9a-f]{4}__) pattern
*/
public static char unescape(@NotNull String escapeSequence) {
Matcher matcher = ESCAPED_CHAR_PATTERN.matcher(escapeSequence);
if (matcher.matches() && matcher.groupCount() == 1) {
String toProcess = escapeSequence.replace("__", "");
return (char) Integer.parseInt(toProcess, 16);
}
throw new IllegalArgumentException(
String.format("String '%s' does not match pattern %s.", escapeSequence, ESCAPED_CHAR_PATTERN.pattern()));
}
/**
* Provided a string which could contain escape sequences generated through {@link #escapeChar(char)}, this method will unescape all
* such sequences.
*
* @param input a string containing escaped characters
* @return a string with all escaped sequences produced by {@link #escapeChar(char)} replaced by the original character
*/
public static @NotNull String unescapeAll(@NotNull String input) {
String unescaped = input;
Matcher matcher = ESCAPED_CHAR_PATTERN.matcher(unescaped);
while (matcher.find()) {
String group = matcher.group();
char unescapedChar = unescape(group);
unescaped = unescaped.replace(group, Character.toString(unescapedChar));
}
return unescaped;
}
/**
* Converts the given {@code path} to a Java package or fully-qualified class name, depending on the {@code path}'s value.
*
* @param path the path to convert
* @return Java package / fully-qualified class name corresponding to the given path
*/
public static @NotNull String makeJavaPackage(@NotNull String path) {
String[] classNameComponents = path.split("/|\\\\");
StringBuilder legalClassNames = new StringBuilder();
for (int i = 0; i < classNameComponents.length; i++) {
String classNameComponent = classNameComponents[i];
if (classNameComponent.isEmpty()) {
continue;
}
legalClassNames.append(getJavaIdentifier(classNameComponent));
if (i < classNameComponents.length - 1) {
legalClassNames.append('.');
}
}
return legalClassNames.toString();
}
/**
* Test whether the argument is a Java keyword, according to the
* <a href="https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html#jls-3.9">Java Language Specification</a>.
*
* @param key the String to test
* @return {@code true} if the String is a Java keyword, {@code false} otherwise
*/
public static boolean isJavaKeyword(@NotNull String key) {
return javaKeywords.contains(key);
}
/**
* Test whether the argument is a Java literal, according to the
* <a href="https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html#jls-3.9">Java Language Specification</a>..
*
* @param key the String to test
* @return {@code true} if the String is a Java literal, {@code false} otherwise
*/
public static boolean isJavaLiteral(@NotNull String key) {
return literals.contains(key);
}
/**
* Test whether the argument is a special identifier, according to the
* a href="https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html#jls-3.9">Java Language Specification</a>..
*
* @param key the String to test
* @return {@code true} if the String is a Java special identifier, {@code false} otherwise
*/
public static boolean isSpecialIdentifier(@NotNull String key) {
return specialIdentifiers.contains(key);
}
}