Merge branch 'cheelio-FieldNameConstants'

This commit is contained in:
Reinier Zwitserloot committed 2018-04-05 14:22:53 +02:00
commit 675699fccb
23 files changed
+450 -5

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ Liu DongMiao <liudongmiao@gmail.com>
Luan Nico <luannico27@gmail.com>
Maarten Mulders <mthmulders@users.noreply.github.com>
Mart Hagenaars <marthagenaars@gmail.com>
Michiel Verheul <cheelio@gmail.com>
Peter Grant <petercgrant@users.noreply.github.com>
Philipp Eichhorn <peichhor@web.de>
Rabea Gransberger <rgra@users.noreply.github.com>
+4 -2
View File
@@ -3,9 +3,11 @@ Lombok Changelog
### v1.16.21 "Edgy Guinea Pig"
* v1.16.20 is the latest stable release of Project Lombok.
* FEATURE: `@FieldNameConstants` is an new feature that generates string constants for your field names. [Docs on @FieldNameConstants](https://projectlombok.org/features/experimental/FieldNameConstants).
* PLATFORM: Fix for using lombok together with JDK9's new `module-info.java` feature. [Issue #985](https://github.com/rzwitserloot/lombok/issues/985)
* PLATFORM: Some initial work on supporting JDK10.
* BUGFIX: Potential fix for Netbeans < 9. [Issue #1555](https://github.com/rzwitserloot/lombok/issues/1555)
* PLATFORM: Some initial work on supporting JDK10 and JDK11.
* BUGFIX: Solved some issues in eclipse that resulted in error 'A save participant caused problems'. [Issue #879](https://github.com/rzwitserloot/lombok/issues/879)
* BUGFIX: Bugfix for Netbeans < 9. [Issue #1555](https://github.com/rzwitserloot/lombok/issues/1555)
* PROMOTION: `var` has been promoted from experimental to the main package with no changes. The 'old' experimental one is still around but is deprecated, and is an alias for the new main package one. [var documentation](https://projectlombok.org/features/var.html).
* OLD-CRUFT: `lombok.experimental.Builder` and `lombok.experimental.Value` are deprecated remnants of when these features were still in experimental. They are now removed entirely. If your project is dependent on an older version of lombok which still has those; fret not, lombok still processes these annotations. It just no longer includes them in the jar.
+8
View File
@@ -499,6 +499,14 @@ public class ConfigurationKeys {
*/
public static final ConfigurationKey<FlagUsageType> UTILITY_CLASS_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.utilityClass.flagUsage", "Emit a warning or error if @UtilityClass is used.") {};
// ----- FieldNameConstants -----
/**
* lombok configuration: {@code lombok.fieldNameConstants.flagUsage} = {@code WARNING} | {@code ERROR}.
*
* If set, <em>any</em> usage of {@code @FieldNameConstants} results in a warning / error.
*/
public static final ConfigurationKey<FlagUsageType> FIELD_NAME_CONSTANTS_FLAG_USAGE = new ConfigurationKey<FlagUsageType>("lombok.fieldNameConstants.flagUsage", "Emit a warning or error if @FieldNameConstants is used.") {};
// ----- Wither -----
/**
@@ -441,4 +441,16 @@ public class HandlerUtil {
}
return String.format("%s%s", prefix, suffix);
}
public static String camelCaseToConstant(String fieldName) {
if (fieldName == null || fieldName.isEmpty()) return "";
StringBuilder b = new StringBuilder();
b.append(Character.toUpperCase(fieldName.charAt(0)));
for (int i = 1; i < fieldName.length(); i++) {
char c = fieldName.charAt(i);
if (Character.isUpperCase(c)) b.append('_');
b.append(Character.toUpperCase(c));
}
return b.toString();
}
}
@@ -1097,7 +1097,7 @@ public class EclipseHandlerUtil {
public static boolean filterField(FieldDeclaration declaration, boolean skipStatic) {
// Skip the fake fields that represent enum constants.
if (declaration.initialization instanceof AllocationExpression &&
((AllocationExpression)declaration.initialization).enumConstant != null) return false;
((AllocationExpression) declaration.initialization).enumConstant != null) return false;
if (declaration.type == null) return false;
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2014-2018 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.eclipse.handlers;
import static lombok.core.handlers.HandlerUtil.handleExperimentalFlagUsage;
import static lombok.eclipse.handlers.EclipseHandlerUtil.*;
import java.lang.reflect.Modifier;
import java.util.Collection;
import lombok.AccessLevel;
import lombok.ConfigurationKeys;
import lombok.core.AST.Kind;
import lombok.core.AnnotationValues;
import lombok.core.handlers.HandlerUtil;
import lombok.eclipse.Eclipse;
import lombok.eclipse.EclipseAnnotationHandler;
import lombok.eclipse.EclipseNode;
import lombok.experimental.FieldNameConstants;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.mangosdk.spi.ProviderFor;
@ProviderFor(EclipseAnnotationHandler.class)
public class HandleFieldNameConstants extends EclipseAnnotationHandler<FieldNameConstants> {
public void generateFieldNameConstantsForType(EclipseNode typeNode, EclipseNode errorNode, AccessLevel level) {
TypeDeclaration typeDecl = null;
if (typeNode.get() instanceof TypeDeclaration) typeDecl = (TypeDeclaration) typeNode.get();
int modifiers = typeDecl == null ? 0 : typeDecl.modifiers;
boolean notAClass = (modifiers & (ClassFileConstants.AccInterface | ClassFileConstants.AccAnnotation)) != 0;
if (typeDecl == null || notAClass) {
errorNode.addError("@FieldNameConstants is only supported on a class, an enum, or a field.");
return;
}
for (EclipseNode field : typeNode.down()) {
if (fieldQualifiesForFieldNameConstantsGeneration(field)) generateFieldNameConstantsForField(field, errorNode.get(), level);
}
}
private void generateFieldNameConstantsForField(EclipseNode fieldNode, ASTNode pos, AccessLevel level) {
if (hasAnnotation(FieldNameConstants.class, fieldNode)) return;
createFieldNameConstantsForField(level, fieldNode, fieldNode, pos, false);
}
private boolean fieldQualifiesForFieldNameConstantsGeneration(EclipseNode field) {
if (field.getKind() != Kind.FIELD) return false;
FieldDeclaration fieldDecl = (FieldDeclaration) field.get();
return filterField(fieldDecl);
}
public void handle(AnnotationValues<FieldNameConstants> annotation, Annotation ast, EclipseNode annotationNode) {
handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.FIELD_NAME_CONSTANTS_FLAG_USAGE, "@FieldNameConstants");
EclipseNode node = annotationNode.up();
FieldNameConstants annotatationInstance = annotation.getInstance();
AccessLevel level = annotatationInstance.level();
if (node == null) return;
switch (node.getKind()) {
case FIELD:
if (level != AccessLevel.NONE) createFieldNameConstantsForFields(level, annotationNode.upFromAnnotationToFields(), annotationNode, annotationNode.get(), true);
break;
case TYPE:
if (level == AccessLevel.NONE) {
annotationNode.addWarning("type-level '@FieldNameConstants' does not work with AccessLevel.NONE.");
return;
}
generateFieldNameConstantsForType(node, annotationNode, level);
break;
}
}
private void createFieldNameConstantsForFields(AccessLevel level, Collection<EclipseNode> fieldNodes, EclipseNode errorNode, ASTNode source, boolean whineIfExists) {
for (EclipseNode fieldNode : fieldNodes) createFieldNameConstantsForField(level, fieldNode, errorNode, source, whineIfExists);
}
private void createFieldNameConstantsForField(AccessLevel level, EclipseNode fieldNode, EclipseNode errorNode, ASTNode source, boolean whineIfExists) {
if (fieldNode.getKind() != Kind.FIELD) {
errorNode.addError("@FieldNameConstants is only supported on a class, an enum, or a field");
return;
}
FieldDeclaration field = (FieldDeclaration) fieldNode.get();
String fieldName = new String(field.name);
String constantName = HandlerUtil.camelCaseToConstant(fieldName);
if (constantName.equals(fieldName)) {
fieldNode.addWarning("Not generating constant for this field: The name of the constant would be equal to the name of this field.");
return;
}
int pS = source.sourceStart, pE = source.sourceEnd;
long p = (long) pS << 32 | pE;
FieldDeclaration fieldConstant = new FieldDeclaration(constantName.toCharArray(), pS,pE);
fieldConstant.bits |= Eclipse.ECLIPSE_DO_NOT_TOUCH_FLAG;
fieldConstant.modifiers = toEclipseModifier(level) | Modifier.STATIC | Modifier.FINAL;
fieldConstant.type = new QualifiedTypeReference(TypeConstants.JAVA_LANG_STRING, new long[] {p,p,p});
fieldConstant.initialization = new StringLiteral(field.name, pS, pE, 0);
injectField(fieldNode.up(), fieldConstant);
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2014-2018 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.experimental;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import lombok.AccessLevel;
/**
* Generates String constants containing the field name for each field.
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface FieldNameConstants {
lombok.AccessLevel level() default AccessLevel.PUBLIC;
}
@@ -0,0 +1,131 @@
/*
* Copyright (C) 2014-2018 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.javac.handlers;
import static lombok.core.handlers.HandlerUtil.handleExperimentalFlagUsage;
import static lombok.javac.handlers.JavacHandlerUtil.*;
import java.lang.reflect.Modifier;
import java.util.Collection;
import lombok.AccessLevel;
import lombok.ConfigurationKeys;
import lombok.core.AST.Kind;
import lombok.core.handlers.HandlerUtil;
import lombok.core.AnnotationValues;
import lombok.experimental.FieldNameConstants;
import lombok.javac.JavacAnnotationHandler;
import lombok.javac.JavacNode;
import lombok.javac.JavacTreeMaker;
import org.mangosdk.spi.ProviderFor;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
@ProviderFor(JavacAnnotationHandler.class)
public class HandleFieldNameConstants extends JavacAnnotationHandler<FieldNameConstants> {
public void generateFieldNameConstantsForType(JavacNode typeNode, JavacNode errorNode, AccessLevel level) {
JCClassDecl typeDecl = null;
if (typeNode.get() instanceof JCClassDecl) typeDecl = (JCClassDecl) typeNode.get();
long modifiers = typeDecl == null ? 0 : typeDecl.mods.flags;
boolean notAClass = (modifiers & (Flags.INTERFACE | Flags.ANNOTATION)) != 0;
if (typeDecl == null || notAClass) {
errorNode.addError("@FieldNameConstants is only supported on a class, an enum, or a field.");
return;
}
for (JavacNode field : typeNode.down()) {
if (fieldQualifiesForFieldNameConstantsGeneration(field)) generateFieldNameConstantsForField(field, errorNode.get(), level);
}
}
private void generateFieldNameConstantsForField(JavacNode fieldNode, DiagnosticPosition pos, AccessLevel level) {
if (hasAnnotation(FieldNameConstants.class, fieldNode)) return;
createFieldNameConstantsForField(level, fieldNode, fieldNode, false);
}
private boolean fieldQualifiesForFieldNameConstantsGeneration(JavacNode field) {
if (field.getKind() != Kind.FIELD) return false;
JCVariableDecl fieldDecl = (JCVariableDecl) field.get();
if (fieldDecl.name.toString().startsWith("$")) return false;
if ((fieldDecl.mods.flags & Flags.STATIC) != 0) return false;
return true;
}
public void handle(AnnotationValues<FieldNameConstants> annotation, JCAnnotation ast, JavacNode annotationNode) {
handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.FIELD_NAME_CONSTANTS_FLAG_USAGE, "@FieldNameConstants");
Collection<JavacNode> fields = annotationNode.upFromAnnotationToFields();
deleteAnnotationIfNeccessary(annotationNode, FieldNameConstants.class);
deleteImportFromCompilationUnit(annotationNode, "lombok.AccessLevel");
JavacNode node = annotationNode.up();
FieldNameConstants annotatationInstance = annotation.getInstance();
AccessLevel level = annotatationInstance.level();
if (node == null) return;
switch (node.getKind()) {
case FIELD:
if (level != AccessLevel.NONE) createFieldNameConstantsForFields(level, fields, annotationNode, annotationNode, true);
break;
case TYPE:
if (level == AccessLevel.NONE) {
annotationNode.addWarning("type-level '@FieldNameConstants' does not work with AccessLevel.NONE.");
return;
}
generateFieldNameConstantsForType(node, annotationNode, level);
break;
}
}
private void createFieldNameConstantsForFields(AccessLevel level, Collection<JavacNode> fieldNodes, JavacNode annotationNode, JavacNode errorNode, boolean whineIfExists) {
for (JavacNode fieldNode : fieldNodes) createFieldNameConstantsForField(level, fieldNode, errorNode, whineIfExists);
}
private void createFieldNameConstantsForField(AccessLevel level, JavacNode fieldNode, JavacNode source, boolean whineIfExists) {
if (fieldNode.getKind() != Kind.FIELD) {
source.addError("@FieldNameConstants is only supported on a class, an enum, or a field");
return;
}
JCVariableDecl field = (JCVariableDecl) fieldNode.get();
String fieldName = field.name.toString();
String constantName = HandlerUtil.camelCaseToConstant(fieldName);
if (constantName.equals(fieldName)) {
fieldNode.addWarning("Not generating constant for this field: The name of the constant would be equal to the name of this field.");
return;
}
JavacTreeMaker treeMaker = fieldNode.getTreeMaker();
JCModifiers modifiers = treeMaker.Modifiers(toJavacModifier(level) | Modifier.STATIC | Modifier.FINAL);
JCExpression returnType = chainDots(fieldNode, "java", "lang", "String");
JCExpression init = treeMaker.Literal(fieldNode.getName());
JCVariableDecl fieldConstant = treeMaker.VarDef(modifiers, fieldNode.toName(constantName), returnType, init);
injectField(fieldNode.up(), fieldConstant);
}
}
+1 -1
View File
@@ -243,7 +243,7 @@ public abstract class AbstractRunTests {
int size = Math.min(expectedLines.length, actualLines.length);
if (size == 0 && expectedLines.length + actualLines.length > 0) {
Assert.fail("Missing / empty expected file.");
Assert.fail("Missing / empty expected file: " + name);
}
for (int i = 0; i < size; i++) {
@@ -1,3 +1,4 @@
//version 7:
public class EqualsAndHashCodeWithGenericsOnInners<A> {
class Inner<B> {
int x;
@@ -0,0 +1,8 @@
public class FieldNameConstantsBasic {
protected static final java.lang.String I_AM_A_DVD_PLAYER = "iAmADvdPlayer";
public static final java.lang.String BUT_PRINT_ME_PLEASE = "butPrintMePlease";
String iAmADvdPlayer;
int $skipMe;
static double skipMeToo;
String butPrintMePlease;
}
@@ -0,0 +1,4 @@
public class FieldNameConstantsWeird {
String iAmADvdPlayer;
String X;
}
@@ -1,3 +1,4 @@
//version 8:
class A {
class B {
String s;
@@ -48,4 +49,4 @@ class C {
return "C.D(a=" + this.getA() + ")";
}
}
}
}
@@ -0,0 +1,15 @@
import lombok.experimental.FieldNameConstants;
import lombok.AccessLevel;
public @FieldNameConstants class FieldNameConstantsBasic {
public static final java.lang.String BUT_PRINT_ME_PLEASE = "butPrintMePlease";
protected static final java.lang.String I_AM_A_DVD_PLAYER = "iAmADvdPlayer";
@FieldNameConstants(level = AccessLevel.PROTECTED) String iAmADvdPlayer;
int $skipMe;
static double skipMeToo;
String butPrintMePlease;
<clinit>() {
}
public FieldNameConstantsBasic() {
super();
}
}
@@ -0,0 +1,9 @@
import lombok.experimental.FieldNameConstants;
import lombok.AccessLevel;
public @FieldNameConstants class FieldNameConstantsWeird {
@FieldNameConstants(level = AccessLevel.NONE) String iAmADvdPlayer;
String X;
public FieldNameConstantsWeird() {
super();
}
}
@@ -0,0 +1,11 @@
import lombok.experimental.FieldNameConstants;
import lombok.AccessLevel;
@FieldNameConstants
public class FieldNameConstantsBasic {
@FieldNameConstants(level = AccessLevel.PROTECTED)
String iAmADvdPlayer;
int $skipMe;
static double skipMeToo;
String butPrintMePlease;
}
@@ -0,0 +1,9 @@
import lombok.experimental.FieldNameConstants;
import lombok.AccessLevel;
@FieldNameConstants
public class FieldNameConstantsWeird {
@FieldNameConstants(level = AccessLevel.NONE)
String iAmADvdPlayer;
String X;
}
@@ -0,0 +1 @@
8 Not generating constant for this field: The name of the constant would be equal to the name of this field.
@@ -0,0 +1 @@
8 Not generating constant for this field: The name of the constant would be equal to the name of this field.
@@ -0,0 +1,7 @@
public class FieldNameConstantsExample {
public static final String I_AM_A_FIELD = "iAmAField";
static final String AND_SO_AM_I = "andSoAmI";
private final String iAmAField;
private final int andSoAmI;
}
@@ -0,0 +1,9 @@
import lombok.experimental.FieldNameConstants;
import lombok.AccessLevel;
@FieldNameConstants
public class FieldNameConstantsExample {
private final String iAmAField;
@FieldNameConstants(level = AccessLevel.PACKAGE)
private final int andSoAmI;
}
@@ -0,0 +1,44 @@
<#import "../_features.html" as f>
<@f.scaffold title="@FieldNameConstants" logline="Name... that... field! String constants for your field's names.">
<@f.history>
<p>
@FieldNameConstants was introduced as experimental feature in lombok v2.0.0.
</p>
</@f.history>
<@f.experimental>
<ul>
<li>
New feature; unsure if this busts enough boilerplate.
</li>
</ul>
Current status: <em>neutral</em> - As a just-introduced feature we're still gathering feedback.
</@f.experimental>
<@f.overview>
<p>
The <code>@FieldNameConstants</code> annotation generates string constants (fields marked <code>public static final</code>, of type <code>java.lang.String</code>) containing the field's name, as a string. This is useful for various marshalling and serialization frameworks. The constant field has the same as the field it represents, except with all uppercase letters, with underscores in front of the uppercase letters in the original field. If this results in the exact same name, the constant is not generated (a warning is generated instead).
</p><p>
The <code>public</code> access modifier can be changed via the parameter <code>level = AccessLevel.PACKAGE</code> for example. You can force a field to be skipped by supplying <code>level = AccessLevel.NONE</code>.
</p><p>
Can be applied to classes (in which case every field gets a constant), or to an individual field.
</p>
</@f.overview>
<@f.snippets name="experimental/FieldNameConstants" />
<@f.confKeys>
<dt>
<code>lombok.fieldNameConstants.flagUsage</code> = [<code>warning</code> | <code>error</code>] (default: not set)
</dt><dd>
Lombok will flag any usage of <code>@FieldDefaults</code> as a warning or error if configured.
</dd>
</@f.confKeys>
<@f.smallPrint>
<p>
Like other lombok handlers that touch fields, any field whose name starts with a dollar (<code>$</code>) symbol is skipped entirely. Such a field will not be modified at all. Static fields are also skipped.
</p>
</@f.smallPrint>
</@f.scaffold>
@@ -62,6 +62,10 @@
<@main.feature title="@Helper" href="Helper">
With a little help from my friends... Helper methods for java.
</@main.feature>
<@main.feature title="@FieldNameConstants" href="FieldNameConstants">
Name... that... field! String constants for your field's names.
</@main.feature>
</div>
<@f.confKeys>