org.netbeans.modules.i18n.java.JavaI18nFinder Maven / Gradle / Ivy
/*
* 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.netbeans.modules.i18n.java;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.text.BadLocationException;
import javax.swing.text.Position;
import javax.swing.text.StyledDocument;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.modules.i18n.HardCodedString;
import org.netbeans.modules.i18n.I18nSupport.I18nFinder;
import org.netbeans.modules.i18n.I18nUtil;
import org.netbeans.modules.i18n.regexp.ParseException;
import org.netbeans.modules.i18n.regexp.Translator;
import org.netbeans.modules.properties.UtilConvert;
import org.openide.DialogDisplayer;
import org.openide.ErrorManager;
import org.openide.NotifyDescriptor;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
/*
* This class was originally written as a static nested class of class
* {@link JavaI18nSupport}.
*/
/**
* Finder which search hard coded strings in java sources.
*
* @author Peter Zavadsky
*/
public class JavaI18nFinder implements I18nFinder {
/** State when finder is in normal java code. */
protected static final int STATE_JAVA = 0;
/** State when finder is at backslash in normal java code. */
protected static final int STATE_JAVA_A_SLASH = 1;
/** State when finder is in line comment. */
protected static final int STATE_LINECOMMENT = 2;
/** State when finder is in block comment. */
protected static final int STATE_BLOCKCOMMENT = 3;
/** State when finder is at star in block commnet. */
protected static final int STATE_BLOCKCOMMENT_A_STAR = 4;
/** State when finder is in string found in nornal java code. */
protected static final int STATE_STRING = 5;
/** State when finder is at backslash in string. */
protected static final int STATE_STRING_A_BSLASH = 6;
/** State when finder is in char in noraml java code. */
protected static final int STATE_CHAR = 7; // to avoid misinterpreting of '"' resp. '\"' char.
/** Document on which the search is performed. */
protected StyledDocument document;
/** Keeps current state. */
protected int state;
/** Flag of search type, if it is searched for i18n-ized strings or non-i18n-ized ones. */
protected boolean i18nSearch;
/** Keeps position from last search iteration. */
protected Position lastPosition;
/** Helper variable for keeping the java string (means pure java code, no coments etc.). */
protected StringBuffer lastJavaString;
/** Helper variable. Buffer at which perform search. */
protected char[] buffer;
/** Helper variable. Actual position of search in buffer. */
protected int position;
/** Helper variable. Start of actual found hard coded string or -1. */
protected int currentStringStart;
/** Helper variable. End of actual found hard coded string or -1. */
protected int currentStringEnd;
/** Helper variable. Used to recognize "a" + "b" as "ab" (bug 185645). */
private boolean concatenatedStringsFound;
/** Helper variable. Used to recognize "a" + variable [+ "b"]* (bug 33759). */
public final String strAndVarFound = "$strAndVarFound$"; //NOI18N
/** Constructs finder. */
public JavaI18nFinder(StyledDocument document) {
super();
this.document = document;
init();
}
/** Initializes finder. */
private void init() {
state = STATE_JAVA;
initJavaStringBuffer();
lastPosition = null;
concatenatedStringsFound = false;
}
/** Resets finder. */
protected void reset() {
init();
}
/**
* Implements I18nFinder
interface method.
* Finds all non-internationalized hard coded strings in source document. */
public HardCodedString[] findAllHardCodedStrings() {
reset();
i18nSearch = false;
return findAllStrings();
}
/**
* Implements I18nFinder
inetrface method.
* Finds hard coded non-internationalized string in buffer.
* @return next HardCodedString
or null if there is no more one.
*/
public HardCodedString findNextHardCodedString() {
i18nSearch = false;
return findNextString();
}
/**
* Implements I18nFinder
interface method.
* Finds all internationalized hard coded strings in source document.
* It's used in test tool. */
public HardCodedString[] findAllI18nStrings() {
reset();
i18nSearch = true;
return findAllStrings();
}
/**
* Implements I18nFinder
inetrface method.
* Finds hard coded internationalized string in buffer. It's used in test tool.
* @return next HardCodedString
or null if there is no more one.
*/
public HardCodedString findNextI18nString() {
i18nSearch = true;
return findNextString();
}
/** Finds all strings according specified regular expression. */
protected HardCodedString[] findAllStrings() {
List list = new ArrayList();
HardCodedString hardString;
while ((hardString = findNextString()) != null) {
list.add(hardString);
}
return !list.isEmpty()
? list.toArray(new HardCodedString[0])
: null;
}
protected HardCodedString findNextString() {
// Reset buffer.
try {
buffer = document.getText(0, document.getLength()).toCharArray();
} catch (BadLocationException ble) {
if (Boolean.getBoolean("netbeans.debug.exception")) { //NOI18N
ble.printStackTrace();
}
return null;
}
// Initialize position.
position = (lastPosition == null)
? 0
: lastPosition.getOffset();
// Reset hard coded string offsets.
currentStringStart = -1;
currentStringEnd = -1;
// Now serious work.
while (position < buffer.length) {
char ch = buffer[position];
// Other chars than '\n' (new line).
if (ch != '\n') {
HardCodedString foundHardString = handleCharacter(ch);
if (foundHardString != null) {
return foundHardString;
}
} else {
handleNewLineCharacter();
}
position++;
} // End of while.
// Indicate end was reached and nothing found.
return null;
}
/** Handles state changes according next charcter. */
protected HardCodedString handleCharacter(char character) {
if (state == STATE_JAVA) {
return handleStateJava(character);
} else if (state == STATE_JAVA_A_SLASH) {
return handleStateJavaASlash(character);
} else if (state == STATE_CHAR) {
return handleStateChar(character);
} else if (state == STATE_STRING_A_BSLASH) {
return handleStateStringABSlash(character);
} else if (state == STATE_LINECOMMENT) {
return handleStateLineComment(character);
} else if (state == STATE_BLOCKCOMMENT) {
return handleStateBlockComment(character);
} else if (state == STATE_BLOCKCOMMENT_A_STAR) {
return handleStateBlockCommentAStar(character);
} else if (state == STATE_STRING) {
return handleStateString(character);
}
return null;
}
/** Handles state when new line '\n' char occures. */
protected void handleNewLineCharacter() {
// New line char '\n' -> reset the state.
if (state == STATE_JAVA
|| state == STATE_JAVA_A_SLASH
|| state == STATE_CHAR
|| state == STATE_LINECOMMENT
|| state == STATE_STRING
|| state == STATE_STRING_A_BSLASH) {
initJavaStringBuffer();
currentStringStart = -1;
currentStringEnd = -1;
state = STATE_JAVA;
} else if (state == STATE_BLOCKCOMMENT
|| state == STATE_BLOCKCOMMENT_A_STAR) {
state = STATE_BLOCKCOMMENT;
}
}
/** Handles state STATE_JAVA
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateJava(char character) {
lastJavaString.append(character);
if (character == '/') {
state = STATE_JAVA_A_SLASH;
} else if (character == '\"') {
state = STATE_STRING;
if (currentStringStart == -1) {
// Found start of hard coded string.
currentStringStart = position;
}
} else if (character == '\'') {
state = STATE_CHAR;
}
return null;
}
/** Handles state STATE_JAVA_A_SLASH
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateJavaASlash(char character) {
lastJavaString.append(character);
if (character == '/') {
state = STATE_LINECOMMENT;
} else if (character == '*') {
state = STATE_BLOCKCOMMENT;
}
return null;
}
/** Handles state STATE_CHAR
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateChar(char character) {
lastJavaString.append(character);
if (character == '\'') {
state = STATE_JAVA;
}
return null;
}
/** Handles state STATE_STRING_A_BSLASH
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateStringABSlash(char character) {
state = STATE_STRING;
return null;
}
/** Handles state STATE_LINECOMMENT
.
* @param character char to proceede
* @return null */
protected HardCodedString handleStateLineComment(char character) {
return null;
}
/** Handles state STATE_BLOCKCOMMENT
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateBlockComment(char character) {
if (character == '*') {
state = STATE_BLOCKCOMMENT_A_STAR;
}
return null;
}
/** Handles state STATE_BLOCKCOMMENT_A_STAR
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateBlockCommentAStar(char character) {
if (character == '/') {
state = STATE_JAVA;
initJavaStringBuffer();
} else if (character != '*') {
state = STATE_BLOCKCOMMENT;
}
return null;
}
/** Handles state STATE_STRING
.
* @param character char to proceede
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStateString(char character) {
if (character == '\\') {
state = STATE_STRING_A_BSLASH;
} else if (character == '\"') {
state = STATE_JAVA;
if ((currentStringEnd == -1) && (currentStringStart != -1)) {
// Found end of hard coded string.
currentStringEnd = position + 1;
int foundStringLength = currentStringEnd - currentStringStart;
try {
// Get hard coded string.
Position hardStringStart = document.createPosition(currentStringStart);
Position hardStringEnd = document.createPosition(currentStringEnd);
String hardString = document.getText(hardStringStart.getOffset(),
foundStringLength);
// Retrieve offset of the end of line where was found hard coded string.
String restBuffer = new String(buffer,
currentStringEnd,
buffer.length - currentStringEnd);
int endOfLine = restBuffer.indexOf('\n');
if (endOfLine == -1) {
endOfLine = restBuffer.length();
}
if (concatenatedStringsFound) {
lastJavaString.append(document.getText(currentStringStart + 1, hardString.length()).replace("\" + \"", "")); //NOI18N
} else {
lastJavaString.append(document.getText(currentStringStart + 1, hardString.length()));
}
// Get the rest of line.
String restOfLine = document.getText(currentStringStart + 1 + hardString.length(),
currentStringEnd + endOfLine - currentStringStart - hardString.length());
if(restOfLine.trim().startsWith("+ \"")) { //NOI18N
concatenatedStringsFound = true;
currentStringEnd = -1;
state = STATE_STRING;
position += 4;
lastJavaString = lastJavaString.delete(lastJavaString.lastIndexOf("\"") - 1, lastJavaString.length()); //NOI18N
return null;
} if(restOfLine.trim().startsWith("+ ")) { // NOI18N
//Handle Bug 33759 (http://netbeans.org/bugzilla/show_bug.cgi?id=33759)
return handleStringWithVariable(hardString, restOfLine);
}
// Replace rest of occurences of \" to cheat out regular expression for very minor case when the same string is after our at the same line.
lastJavaString.append(restOfLine.replace('\"', '_'));
if (concatenatedStringsFound) {
concatenatedStringsFound = false;
hardString = hardString.replace("\" + \"", ""); //NOI18N
}
// If not matches regular expression -> is not internationalized.
if (isSearchedString(lastJavaString.toString(), hardString)) {
lastPosition = hardStringEnd;
// Search was successful -> return.
return new HardCodedString(extractString(hardString),
hardStringStart,
hardStringEnd);
}
} catch (BadLocationException ble) {
ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL,
ble);
} finally {
if (state == STATE_JAVA) {
currentStringStart = -1;
currentStringEnd = -1;
initJavaStringBuffer();
}
}
}
}
return null;
}
/** Handles the situation where a string is followed by a variable which again is followed by a String or variable (bug 33759).
* @param hardString String found so far
* @param restOfLine String found till the end of the current line
* @return HardCodedString
or null if not found yet */
protected HardCodedString handleStringWithVariable(String hardString, String restOfLine) {
try {
Position hardStringStart = document.createPosition(currentStringStart);
Position hardStringEnd = document.createPosition(currentStringEnd);
// get rest of statement. Stmt ends on , or ; outside of string.
String regex = "[%s](?=([^\"]*\"[^\"]*\")*[^\"]*$)";
String[] tokens = restOfLine.split(String.format(regex, ",;"), -1);
restOfLine = tokens[0];
// Remove last "
hardString = hardString.substring(0, hardString.length() - 1);
String[] splits = restOfLine.substring(1).split(String.format(regex, "\\+"), -1); // NOI18N
String split = ""; // NOI18N
for (int i = 0; i < splits.length; i++) {
split = splits[i];
if (split.trim().startsWith("\"")) { // NOI18N
if(!hardString.endsWith(strAndVarFound)) {
hardString = hardString.concat(strAndVarFound + strAndVarFound);
}
hardString = hardString.concat(split.trim().substring(1, split.trim().lastIndexOf("\""))); // NOI18N
} else {
hardString = hardString.concat(strAndVarFound + split + strAndVarFound);
}
}
hardString = hardString.concat("\""); // NOI18N
if (split.lastIndexOf("\"") == -1) { // NOI18N
currentStringEnd += restOfLine.indexOf(split) + split.length() + (split.endsWith(" ") ? 0 : 1); // NOI18N
} else {
currentStringEnd += restOfLine.indexOf(split) + split.lastIndexOf("\"") + 2; // NOI18N
}
hardStringEnd = document.createPosition(currentStringEnd);
lastPosition = hardStringEnd;
// Search was successful -> return.
return new HardCodedString(extractString(hardString),
hardStringStart,
hardStringEnd);
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
/** Modifies the text of a HardCodedString
so that it represents the actual text found in the editor and also shown in the ResourceWizardPanel
.
* @param hcString HardCodedString
to modify
* @return modified HardCodedString
or null if hcString
does not fall in the category specified by Bug 33759 (http://netbeans.org/bugzilla/show_bug.cgi?id=33759) */
public HardCodedString modifyHCStringText(HardCodedString hcString) {
String hcStr = hcString.getText();
int strAndVarLength = strAndVarFound.length();
if (hcStr.contains(strAndVarFound)) {
String newHcstrText = ""; // NOI18N
int startVar = hcStr.indexOf(strAndVarFound);
int endVar = -1;
int counterVar = 0;
newHcstrText = hcStr.substring(0, startVar);
while (startVar != -1) {
if (counterVar > 0) {
newHcstrText = newHcstrText.concat(" + \"").concat(hcStr.substring(endVar + strAndVarLength, startVar)); // NOI18N
}
endVar = hcStr.indexOf(strAndVarFound, startVar + strAndVarLength);
if(startVar + strAndVarLength == endVar) {
newHcstrText = newHcstrText.concat("\""); // NOI18N
counterVar--;
} else {
newHcstrText = newHcstrText.concat("\" + ").concat(hcStr.substring(startVar + strAndVarLength, endVar).trim()); // NOI18N
}
startVar = hcStr.indexOf(strAndVarFound, endVar + strAndVarLength);
counterVar++;
if (startVar == -1) {
newHcstrText = hcStr.substring(endVar + strAndVarLength).trim().length() == 0 ? newHcstrText : newHcstrText.concat(" + \""); // NOI18N
newHcstrText = newHcstrText.concat(hcStr.substring(endVar + strAndVarLength));
}
}
return new HardCodedString(newHcstrText, hcString.getStartPosition(), hcString.getEndPosition());
}
return null;
}
/** Resets lastJavaString
variable.
* @see #lastJavaString*/
private void initJavaStringBuffer() {
lastJavaString = new StringBuffer();
}
/** Helper utility method. */
private String extractString(String sourceString) {
if (sourceString == null) {
return ""; //NOI18N
}
if ((sourceString.length() >= 2)
&& (sourceString.charAt(0) == '\"')
&& (sourceString.charAt(sourceString.length() - 1) == '\"')) {
sourceString = sourceString.substring(1, sourceString.length() - 1);
}
return sourceString;
}
/**
* Help method for decision if found hard coded string is searched string. It means
* if it is i18n-zed or non-internationalized (depending on i18nSearch
flag.
*
* The part of line
* (starts after previous found hard coded string) with current found hard code string is compared
* against regular expression which can user specify via i18n options. If the compared line matches
* that regular expression the hard coded string is considered as internationalized.
*
* @param partHardLine line of code which includes hard coded string and starts from beginning or
* the end of previous hard coded string.
* @param hardString found hard code string
* @return true if string is internationalized and i18nSearch
flag is true
* or if if string is non-internationalized and i18nSearch
flag is false
*/
protected boolean isSearchedString(String partHardLine, String hardString) {
String lineToMatch = UtilConvert.unicodesToChars(partHardLine);
Boolean regexpTestResult;
Exception ex = null;
try {
String regexp = createRegularExpression(hardString);
regexpTestResult = (Pattern.compile(regexp).matcher(lineToMatch).find()
== i18nSearch); //auto-boxing
} catch (ParseException ex1) {
ex = ex1;
regexpTestResult = null;
} catch (PatternSyntaxException ex2) {
ex = ex2;
regexpTestResult = null;
}
if (Boolean.FALSE.equals(regexpTestResult)) {
/*
* the string is an identifier of a bundle or a key
* of a bundle entry
*/
return false;
}
JavaSource js = JavaSource.forDocument(document);
if (js != null) {
final AnnotationDetector annotationDetector = new AnnotationDetector(currentStringStart);
try {
boolean firstTry = true;
do {
if (!firstTry) {
try {
Thread.sleep(200);
} catch (InterruptedException exInterrupted) {
Exceptions.printStackTrace(exInterrupted);
//but still continue
}
}
annotationDetector.reset();
js.runUserActionTask(annotationDetector, true);
firstTry = false;
} while (annotationDetector.wasCancelled()); // XXX: Does not seem necesary.
} catch (IOException ioEx) {
Exceptions.printStackTrace(ioEx);
}
if (annotationDetector.wasAnnotationDetected()) {
// the string is within an annotation
return false;
}
}
if (regexpTestResult != null) {
/* both tests passed */
assert regexpTestResult.equals(Boolean.TRUE);
return true;
}
/*
* Handle the situation that some syntax error has been detected:
*/
ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ex);
// Indicate error, but allow user what to do with the found hard coded string to be able go thru
// this problem.
// Note: All this shouldn't happen. The reason is 1) bad set reg exp format (in options) or
// 2) it's error in this code.
String msg = NbBundle.getMessage(JavaI18nSupport.class,
"MSG_RegExpCompileError", //NOI18N
hardString);
Object answer = DialogDisplayer.getDefault().notify(
new NotifyDescriptor.Confirmation(
msg,
NotifyDescriptor.YES_NO_OPTION,
NotifyDescriptor.ERROR_MESSAGE));
return NotifyDescriptor.YES_OPTION.equals(answer);
}
/**
* Creates a regular expression matching the pattern specified in the
* module options.
* The pattern specified in the options contains a special token
* {hardString}
. This token is replaced with a regular
* expression matching exactly the string passed as a parameter
* and a result of this substitution is returned.
*
* @param hardString hard-coded string whose regexp-equivalent is
* to be put in place of token
* {hardString}
* @return regular expression matching the pattern specified
* in the module options
*/
private String createRegularExpression(String hardString)
throws ParseException {
String regexpForm;
if (i18nSearch) {
regexpForm = I18nUtil.getOptions().getI18nRegularExpression();
} else {
regexpForm = I18nUtil.getOptions().getRegularExpression();
}
/*
* Translate the regexp form to the JDK's java.util.regex syntax
* and replace tokens "{key}" and "{hardString}" with the passed
* hard-coded string.
*/
Map map = new HashMap(3);
map.put("key", hardString); //older form of regexp format //NOI18N
map.put("hardString", hardString); //NOI18N
return Translator.translateRegexp(regexpForm, map);
}
/**
* Task that determines whether there is a Java annotation at the given
* cursor position. It is made for the situation that it is known that
* there is a string literal at the given cursor position and the goal
* is to check whether that string literal is a part of an annotation.
* It may not work in cases that there is e.g. a numeric constant
* at the current cursor position.
*
* @author Marian Petras
*/
static final class AnnotationDetector implements CancellableTask {
private final int caretPosition;
private volatile boolean cancelled;
private boolean annotationDetected = false;
private AnnotationDetector(int caretPosition) {
this.caretPosition = caretPosition;
}
void reset() {
cancelled = false;
annotationDetected = false;
}
public void run(CompilationController controller) throws IOException {
if (cancelled) {
return;
}
controller.toPhase(Phase.RESOLVED); //cursor position needed
if (cancelled) {
return;
}
TreePath treePath = controller.getTreeUtilities()
.pathFor(caretPosition);
if (treePath == null) {
return;
}
Tree.Kind kind = treePath.getLeaf().getKind();
if (kind == Tree.Kind.STRING_LITERAL) {
if ((treePath = treePath.getParentPath()) == null) {
return;
}
kind = treePath.getLeaf().getKind();
}
if (kind == Tree.Kind.NEW_ARRAY) {
if ((treePath = treePath.getParentPath()) == null) {
return;
}
kind = treePath.getLeaf().getKind();
}
if (kind == Tree.Kind.ASSIGNMENT) {
if ((treePath = treePath.getParentPath()) == null) {
return;
}
kind = treePath.getLeaf().getKind();
}
annotationDetected = (kind == Tree.Kind.ANNOTATION);
}
public void cancel() {
cancelled = true;
}
boolean wasCancelled() {
return cancelled;
}
boolean wasAnnotationDetected() {
return annotationDetected;
}
}
}