blob: f2d84b9f35073ffac2161e7f3d44170cce6dbd98 [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.netbeans.modules.i18n.java;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Scope;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.swing.JPanel;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.StyledDocument;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.GeneratorUtilities;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.JavaSource.Phase;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.modules.i18n.HardCodedString;
import org.netbeans.modules.i18n.InfoPanel;
import org.netbeans.modules.i18n.I18nString;
import org.netbeans.modules.i18n.I18nSupport;
import org.netbeans.modules.i18n.I18nUtil;
import org.netbeans.modules.i18n.PropertyPanel;
import org.netbeans.modules.i18n.ResourceHolder;
import org.openide.loaders.DataObject;
import org.openide.NotifyDescriptor;
import org.openide.DialogDisplayer;
import org.openide.filesystems.FileObject;
import org.openide.text.NbDocument;
import org.openide.util.MapFormat;
import org.openide.util.Lookup;
import org.openide.ErrorManager;
import org.openide.util.NbBundle;
/**
* Support for internationalizing strings in java sources.
*
* @author Peter Zavadsky
* @see I18nSupport
*/
public class JavaI18nSupport extends I18nSupport {
/** Modifiers of field which are going to be internbationalized (default is private static final). */
protected Set<Modifier> modifiers = EnumSet.of(Modifier.PRIVATE,
Modifier.STATIC,
Modifier.FINAL);
/** Identifier of field element pointing to field which defines resource bundle in the source. */
protected String identifier;
/** Generate field? */
protected boolean generateField;
/** Init string format. */
protected String initFormat;
private JavaReplacePanel additionalCustomizer;
/** Constructor.
* @see I18nSupport */
public JavaI18nSupport(DataObject sourceDataObject) {
super(sourceDataObject);
initFormat = I18nUtil.getOptions().getInitJavaCode();
}
/** Creates <code>I18nFinder</code>. Implements superclass abstract method. */
protected I18nFinder createFinder() {
return new JavaI18nFinder(document);
}
/** Creates <code>I18nReplacer</code>. Implemens superclass abstract method. */
protected I18nReplacer createReplacer() {
return new JavaI18nReplacer();
}
/** Creates <code>ResourceHolder</code>. Implemens superclass abstract method. */
protected ResourceHolder createResourceHolder() {
return new JavaResourceHolder();
}
/** Implements superclass abstract method. */
public I18nString getDefaultI18nString(HardCodedString hcString) {
I18nString i18nString = new JavaI18nString(this);
final ResourceHolder resourceHolder
= i18nString.getSupport().getResourceHolder();
if (resourceHolder.getResource() == null) {
DataObject lastResource = I18nUtil.getOptions().getLastResource2(sourceDataObject);
if (lastResource != null) {
FileObject sourceFile = sourceDataObject.getPrimaryFile();
FileObject bundleFile = lastResource.getPrimaryFile();
ClassPath execClassPath = ClassPath.getClassPath(sourceFile,
ClassPath.EXECUTE);
if (execClassPath != null) {
if (execClassPath.getResourceName(bundleFile) != null) {
resourceHolder.setResource(lastResource);
}
}
}
}
if (hcString == null) {
return i18nString;
}
String text = decodeUnicodeSeq(hcString.getText());
String key = text.toUpperCase(); // don't replace ' ' with '_' ! #168798
String hcStr = hcString.getText();
String strAndVar = ((JavaI18nFinder)getFinder()).strAndVarFound;
int strAndVarLength = strAndVar.length();
if(hcStr.contains(strAndVar)) { // handle Bug 33759 (http://netbeans.org/bugzilla/show_bug.cgi?id=33759)
ArrayList<String> variables = new ArrayList<String>();
int startVar = hcStr.indexOf(strAndVar);
int endVar = -1;
int counterVar = 0;
text = hcStr.substring(0, startVar);
while(startVar != -1) {
if(counterVar > 0) {
text = text.concat(hcStr.substring(endVar + strAndVarLength, startVar));
}
endVar = hcStr.indexOf(strAndVar, startVar + strAndVarLength);
if(startVar + strAndVarLength == endVar) {
counterVar--;
} else {
text = text.concat("{").concat(Integer.toString(counterVar)).concat("}"); // NOI18N
variables.add(hcStr.substring(startVar + strAndVarLength, endVar).trim());
}
startVar = hcStr.indexOf(strAndVar, endVar + strAndVarLength);
counterVar++;
if(startVar == -1) {
text = text.concat(hcStr.substring(endVar + strAndVarLength));
}
}
key = text.toUpperCase(); // don't replace ' ' with '_' ! #168798
i18nString.setKey(key);
i18nString.setValue(text);
i18nString.setComment(""); // NOI18N
i18nString.setReplaceFormat(I18nUtil.getReplaceFormatItems().get(4));
String[] arguments = new String[variables.size()];
for (int i = 0; i < variables.size(); i++) {
arguments[i] = variables.get(i);
}
((JavaI18nString)i18nString).setArguments(arguments);
} else {
i18nString.setKey(key);
i18nString.setValue(text);
i18nString.setComment(""); // NOI18N
// If generation of field is set and replace format doesn't include identifier argument replace it with the default with identifier.
if (isGenerateField() && i18nString.getReplaceFormat().indexOf("{identifier}") == -1) { // NOI18N
i18nString.setReplaceFormat(I18nUtil.getReplaceFormatItems().get(0));
}
}
return i18nString;
}
private static final String octalDigitChars
= "01234567"; //NOI18N
private static final String hexaDigitChars
= "0123456789abcdefABCDEF"; //NOI18N
/**
* Translates Java Unicode sequences (<code>&#x5c;u<i>nnnn</i></code>)
* to the corresponding characters.
* @param text text with or without Unicode sequences
* @return the same text with Unicode sequences replaced with corresponding
* characters; may be the same instance as the passed text
* if there were no valid Unicode sequences present in it
* @author Marian Petras
*/
private static String decodeUnicodeSeq(String text) {
final StringBuilder result = new StringBuilder(text.length());
final char[] chars = text.toCharArray();
final int stateInitial = 0;
final int stateBackSlash = 1;
final int stateUnicode = 2;
final int stateOctalValue = 3;
int state = stateInitial;
int unicodeValue = 0;
char[] unicodeValueChars = new char[3];
int valueBytesRead = 0;
int position;
int charIndex = 0;
while (charIndex < chars.length) {
char c = chars[charIndex++];
switch (state) {
case stateInitial:
if (c == '\\') {
state = stateBackSlash;
} else {
result.append(c);
}
break;
case stateBackSlash:
if (c == 'u') {
state = stateUnicode;
} else if ((c >= '0') && (c <= '3')) {
unicodeValue = c - '0';
assert (unicodeValue >= 0) && (unicodeValue <= 3);
valueBytesRead = 1;
state = stateOctalValue;
} else {
result.append('\\').append(c);
state = stateInitial;
}
break;
case stateOctalValue:
position = octalDigitChars.indexOf(c);
if (position >= 0) {
unicodeValue = (unicodeValue << 3) | position;
valueBytesRead++;
} else {
charIndex--; //handle the character in the next round
}
if ((position < 0) || (valueBytesRead == 3)) {
appendChar(result, unicodeValue);
state = stateInitial;
valueBytesRead = 0;
unicodeValue = 0;
}
break;
case stateUnicode:
position = hexaDigitChars.indexOf(c);
if (position >= 0) {
if (position > 15) { //one of [A-F] used
position -= 6; //transform to lowercase
}
assert position <= 15;
unicodeValue = (unicodeValue << 4) | position;
if (++valueBytesRead == 4) {
appendChar(result, unicodeValue);
state = stateInitial;
} else {
unicodeValueChars[valueBytesRead - 1] = c;
/* keep the state at stateUnicode */
}
} else if (c == 'u') {
/*
* Handles \\u.... sequences with multiple
* 'u' characters, such as \\uuu1234 (which is legal).
*/
/* keep the state at stateUnicode */
} else {
/* append the malformed Unicode sequence: */
result.append('\\');
result.append('u');
for (int i = 0; i < valueBytesRead; i++) {
result.append(unicodeValueChars[i]);
}
result.append(c);
state = stateInitial;
}
if (state != stateUnicode) {
valueBytesRead = 0;
unicodeValue = 0;
}
break;
default:
assert false;
throw new IllegalStateException();
} //switch (state)
} //for-loop
switch (state) {
case stateInitial:
break;
case stateBackSlash:
result.append('\\');
break;
case stateOctalValue:
assert (valueBytesRead >= 0) && (valueBytesRead < 3);
appendChar(result, unicodeValue);
break;
case stateUnicode:
/* append the incomplete Unicode sequence: */
assert (valueBytesRead >= 0) && (valueBytesRead < 4);
result.append('\\').append('u');
for (int i = 0; i < valueBytesRead; i++) {
result.append(unicodeValueChars[i]);
}
break;
default:
assert false;
throw new IllegalStateException();
}
return result.toString();
}
/**
* Appends a character to the given buffer.
*
* @param buf buffer to which a character is to be added
* @param unicodeValue Unicode value of the character to be appended;
* must be in range from {@code 0} to {@code 65535}
* @return the passed buffer
*/
private static final StringBuilder appendChar(StringBuilder buf,
int unicodeValue)
throws IllegalArgumentException {
if ((unicodeValue < 0) || (unicodeValue > 0xffff)) {
throw new IllegalArgumentException("value out of range: " //NOI18N
+ unicodeValue);
}
/* append the Unicode character: */
if ((unicodeValue >= 0x20) && (unicodeValue != 0x7f)) {
buf.append((char) unicodeValue);
} else {
buf.append('\\');
switch (unicodeValue) {
case 0x08:
buf.append('b'); //bell
break;
case 0x09:
buf.append('t'); //tab
break;
case 0x0a:
buf.append('n'); //NL
break;
case 0x0c:
buf.append('f'); //FF
break;
case 0x0d:
buf.append('r'); //CR
break;
default:
buf.append('u');
for (int shift = 12; shift >= 0; shift -= 4) {
buf.append(hexaDigitChars.charAt(
((unicodeValue >> shift) & 0xf)));
}
break;
}
}
return buf;
}
/** Implements <code>I18nSupport</code> superclass abstract method. Gets info panel about found hard string. */
public JPanel getInfo(HardCodedString hcString) {
return new JavaInfoPanel(hcString, document);
}
/** Getter for identifier. */
public String getIdentifier() {
if ((identifier == null) || (identifier == "")) { //NOI18N
createIdentifier();
}
return identifier;
}
/** Setter for identifier. */
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
/** Getter for modifiers. */
public Set<Modifier> getModifiers() {
return modifiers;
}
/** Setter for modifiers. */
public void setModifiers(Set<Modifier> modifiers) {
this.modifiers = modifiers;
}
/** Getter for generate field property.*/
public boolean isGenerateField() {
return generateField;
}
/** Setter for generate field property. */
public void setGenerateField(boolean generateField) {
this.generateField = generateField;
}
/** Getter for init format property. */
public String getInitFormat() {
return initFormat;
}
/** Setter for init format property. */
public void setInitFormat(String initFormat) {
this.initFormat = initFormat;
}
/** Overrides superclass method. */
@Override
public PropertyPanel getPropertyPanel() {
return new JavaPropertyPanel();
}
/** Overrides superclass method.
* @return true */
@Override
public boolean hasAdditionalCustomizer() {
return true;
}
/** Overrides superclass method.
* @return <code>JavaReplacePanel</code> which offers to customize additional
* source values (in our case for creating bundle field) */
@Override
public JPanel getAdditionalCustomizer() {
if (additionalCustomizer == null)
additionalCustomizer = new JavaReplacePanel(this);
return additionalCustomizer;
}
/** Overrides superclass method.
* Actuallay creates bundle field specified by user */
@Override
public void performAdditionalChanges() {
// Creates field.
createField();
}
/** Utility method. Creates identifier for this support instance. */
public void createIdentifier() {
String name;
try {
name = resourceHolder.getResource().getName();
} catch (NullPointerException npe) {
identifier = ""; // NOI18N
return;
}
// first letter to lowercase
if (name.length() > 0) {
name = name.substring(0, 1).toLowerCase() + name.substring(1);
} else {
name = name.toLowerCase();
}
identifier = name;
}
/**
* Task that adds a field to the internationalized Java source file.
*/
private final class AddFieldTask implements Task<WorkingCopy> {
/** name of the field to be added */
private final String fieldName;
AddFieldTask(String fieldName) {
this.fieldName = fieldName;
}
public void run(WorkingCopy workingCopy) throws Exception {
final TypeElement sourceClassElem = getClass(workingCopy);
if (sourceClassElem == null) {
return;
}
List<? extends javax.lang.model.element.Element> classMembers
= sourceClassElem.getEnclosedElements();
List<? extends VariableElement> fields
= ElementFilter.fieldsIn(classMembers);
if (containsField(fields, fieldName)) {
return;
}
int targetPosition = findTargetPosition(classMembers, fields);
final TreeMaker treeMaker = workingCopy.getTreeMaker();
final Elements elements = workingCopy.getElements();
final Trees trees = workingCopy.getTrees();
final TreeUtilities treeUtilities = workingCopy.getTreeUtilities();
TypeElement resourceBundleTypeElem = elements.getTypeElement(
"java.util.ResourceBundle");//NOI18N
assert resourceBundleTypeElem != null;
ExpressionTree fieldDefaultValue
= treeUtilities.parseVariableInitializer(getInitString(),
new SourcePositions[1]);
TreePath classTreePath = trees.getPath(sourceClassElem);
Scope classScope = trees.getScope(classTreePath);
if (classScope != null) {
treeUtilities.attributeTree(fieldDefaultValue, classScope);
}
VariableTree field = treeMaker.Variable(
treeMaker.Modifiers(modifiers),
fieldName,
treeMaker.QualIdent(resourceBundleTypeElem),
GeneratorUtilities.get(workingCopy).importFQNs(fieldDefaultValue));
ClassTree oldClassTree = (ClassTree) classTreePath.getLeaf();
ClassTree newClassTree = (targetPosition != -1)
? treeMaker.insertClassMember(oldClassTree, targetPosition, field)
: treeMaker.addClassMember(oldClassTree, field);
workingCopy.rewrite(oldClassTree, newClassTree);
}
/**
* Finds the target position within the source class element.
* In the current implementation, the target position is just below
* the last static field of the class; if there is no static field
* in the class, the target position is the top of the class.
*
* @param classMembers list of all members of the class
* @param fields list of the fields in the class
* @return target position ({@code 0}-based) of the field,
* or {@code -1} if the field should be added to the end
* of the class
*/
private int findTargetPosition(
List<? extends javax.lang.model.element.Element> classMembers,
List<? extends VariableElement> fields) {
if (fields.isEmpty()) {
return 0;
}
int target = 0;
boolean skippingStaticFields = false;
Iterator<? extends javax.lang.model.element.Element> membersIt
= classMembers.iterator();
for (int index = 0; membersIt.hasNext(); index++) {
javax.lang.model.element.Element member = membersIt.next();
ElementKind kind = member.getKind();
if (kind.isField()
&& (kind != ElementKind.ENUM_CONSTANT)
&& member.getModifiers().contains(Modifier.STATIC)) {
/* it is a static field - skip it! */
skippingStaticFields = true;
} else if (skippingStaticFields) {
/* we were skipping all static fields - until now */
skippingStaticFields = false;
target = index;
}
}
return !skippingStaticFields ? target : -1;
}
/**
* Finds a main top-level class or a nested class element
* for {@code sourceDataObject} which should be initialized.
*/
private TypeElement getClass(WorkingCopy workingCopy)
throws IOException {
workingCopy.toPhase(Phase.ELEMENTS_RESOLVED);
final String preferredName = sourceDataObject.getName();
TypeElement firstPublicNestedClass = null;
List<? extends TypeElement> topClasses = workingCopy.getTopLevelElements();
for (TypeElement topElement : topClasses) {
ElementKind elementKind = topElement.getKind();
if (!elementKind.isClass()) {
continue;
}
if (topElement.getSimpleName().contentEquals(preferredName)) {
return topElement;
}
if ((firstPublicNestedClass == null)
&& topElement.getModifiers().contains(Modifier.PUBLIC)) {
firstPublicNestedClass = topElement;
}
}
return firstPublicNestedClass;
}
/**
* Checks whether the given class contains a field of the given name.
*
* @param clazz class that should be searched
* @param fieldName name of the field
* @return {@code true} if the class contains such a field,
* {@code false} otherwise
*/
private boolean containsField(List<? extends VariableElement> fields,
String fieldName) {
if (!fields.isEmpty()) {
for (VariableElement field : fields) {
if (field.getSimpleName().contentEquals(fieldName)) {
return true;
}
}
}
return false;
}
}
/**
* Creates a new field which holds a reference to the resource holder
* (resource bundle). The field is added to the internationalized file.
*/
private void createField() {
// Check if we have to generate field.
if (!isGenerateField()) {
return;
}
final JavaSource javaSource = JavaSource.forDocument(document);
try {
ModificationResult result
= javaSource.runModificationTask(new AddFieldTask(getIdentifier()));
result.commit();
} catch (Exception ex) {
ErrorManager.getDefault().notify(ErrorManager.EXCEPTION, ex);
}
}
/**
* Helper method.
* Gets the string, the piece of code which initializes field resource
* bundle in the source.
* E.g.:
* <pre><code>java.util.ResourceBundle &lt;identifier name&gt;<br />
* = <b>java.util.ResourceBundle.getBundle(&quot;&lt;package name&gt;</b>&quot;)</code></pre>
*
* @return String -&gt; piece of initilizing code.
*/
public String getInitString() {
String initJavaFormat = getInitFormat();
// Create map.
FileObject fo = resourceHolder.getResource().getPrimaryFile();
ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE);
Map<String,String> map = new HashMap<String,String>(3);
map.put("bundleNameSlashes", cp.getResourceName(fo, '/', false));//NOI18N
map.put("bundleNameDots", cp.getResourceName(fo, '.', false)); //NOI18N
map.put("sourceFileName", (sourceDataObject != null)
? sourceDataObject.getPrimaryFile().getName()
: ""); //NOI18N
return MapFormat.format(initJavaFormat, map);
}
/** Replacer for java sources used by enclosing class. */
public static class JavaI18nReplacer implements I18nReplacer {
/** Constructor.*/
public JavaI18nReplacer() {
}
/** Replaces found hard coded string in source.
* @param hcString found hard coded string to-be replaced
* @param rbString holds replacing values */
public void replace(final HardCodedString hcString,
final I18nString i18nString) {
// comment out?
if (i18nString.getKey() == null) {
final StyledDocument document = i18nString.getSupport().getDocument();
NbDocument.runAtomic(
document,
new Runnable() {
public void run() {
try {
// find the end of line
int idx = hcString.getEndPosition().getOffset() + 1;
String text = document.getText(idx, document.getLength() - idx);
for (int i = idx; idx-i < text.length() && text.charAt(idx-i) != '\n' ; idx++);
document.insertString(idx, " //NOI18N", null); //NOI18N
} catch (BadLocationException ble) {
NotifyDescriptor.Message message
= new NotifyDescriptor.Message(
NbBundle.getMessage(JavaI18nSupport.class,
"MSG_CouldNotReplace"),//NOI18N
NotifyDescriptor.ERROR_MESSAGE);
DialogDisplayer.getDefault().notify(message);
}
}
});
return;
}
if (!(i18nString instanceof JavaI18nString)) {
throw new IllegalArgumentException(
"I18N module: i18nString have to be an instance of JavaI18nString.");//NOI18N
}
final String newCode = i18nString.getReplaceString();
final StyledDocument document = i18nString.getSupport().getDocument();
// Call runAtomic method to break guarded flag if it is necessary. (For non-guarded works as well).
NbDocument.runAtomic(
document,
new Runnable() {
public void run() {
try {
if (hcString.getLength() > 0) {
document.remove(hcString.getStartPosition().getOffset(),
hcString.getLength());
}
if (newCode != null && newCode.length() > 0) {
document.insertString(hcString.getEndPosition().getOffset(),
newCode, null);
}
} catch (BadLocationException ble) {
NotifyDescriptor.Message message
= new NotifyDescriptor.Message(
NbBundle.getMessage(JavaI18nSupport.class,
"MSG_CouldNotReplace"),//NOI18N
NotifyDescriptor.ERROR_MESSAGE);
DialogDisplayer.getDefault().notify(message);
}
}
});
}
} // End of nested class JavaI18nReplacer.
/** Panel for showing info about hard coded string. */
private static class JavaInfoPanel extends InfoPanel {
/** Constructor. */
public JavaInfoPanel(HardCodedString hcString, StyledDocument document) {
super(hcString, document);
}
/** Implements superclass abstract method. */
protected void setHardCodedString(HardCodedString hcString, StyledDocument document) {
getStringText().setText(hcString == null ? "" //NOI18N
: hcString.getText());
int pos;
String hardLine;
if (hcString.getStartPosition() == null) {
hardLine = ""; //NOI18N
} else {
pos = hcString.getStartPosition().getOffset();
try {
Element paragraph = document.getParagraphElement(pos);
hardLine = document.getText(paragraph.getStartOffset(),
paragraph.getEndOffset() - paragraph.getStartOffset())
.trim();
} catch (BadLocationException ble) {
hardLine = ""; // NOI18N
}
}
getFoundInText().setText(hardLine);
remove(getComponentLabel());
remove(getComponentText());
remove(getPropertyLabel());
remove(getPropertyText());
}
} // End of JavaInfoPanel inner class.
/** Factory for {@code JavaI18nSupport}. */
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.i18n.I18nSupport.Factory.class)
public static class Factory extends I18nSupport.Factory {
/** Implements interface. */
public I18nSupport createI18nSupport(DataObject dataObject) {
return new JavaI18nSupport(dataObject);
}
/** Gets class of supported <code>DataObject</code>.
* @return <code>JavaDataObject</code> class or <code>null</code>
* if java module is not available */
public Class getDataObjectClass() {
// XXX Cleaner should be this code dependend on java module
// -> I18n API needed.
try {
return Class.forName(
"org.netbeans.modules.java.JavaDataObject", // NOI18N
false,
Lookup.getDefault().lookup(ClassLoader.class));
} catch (ClassNotFoundException cnfe) {
return null;
}
}
} // End of class Factory.
}