All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.i18n.java.JavaI18nFinder Maven / Gradle / Ivy

The newest version!
/*
 * 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; } } }