diff options
-rw-r--r-- | build.xml | 8 | ||||
-rw-r--r-- | buildScripts/eclipse-run-tests.template | 1 | ||||
-rw-r--r-- | buildScripts/ivy.xml | 2 | ||||
-rw-r--r-- | doc/changelog.markdown | 1 | ||||
-rw-r--r-- | src/core/lombok/ConfigurationKeys.java | 9 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java | 53 | ||||
-rw-r--r-- | src/core/lombok/javac/handlers/JavacHandlerUtil.java | 20 | ||||
-rw-r--r-- | test/core/src/lombok/RunTestsViaEcj.java | 1 | ||||
-rw-r--r-- | test/transform/resource/after-delombok/GenerateSuppressFBWarnings.java | 9 | ||||
-rw-r--r-- | test/transform/resource/after-ecj/GenerateSuppressFBWarnings.java | 9 | ||||
-rw-r--r-- | test/transform/resource/before/GenerateSuppressFBWarnings.java | 5 | ||||
-rw-r--r-- | website/features/configuration.html | 25 |
12 files changed, 117 insertions, 26 deletions
@@ -460,7 +460,7 @@ ${sourceWarning}</echo> <echo>WARNING: If you wish to test JDK8 features in eclipse, there must be a JDK8 installation configured in your eclipse, and it must be called 'JavaSE-1.8'.</echo> </target> - <target name="setupJavaOpenJDK6TestEnvironment" depends="contrib" description="Sets up the test so that 'ant test' will test against OpenJDK6."> + <target name="setupJavaOpenJDK6TestEnvironment" depends="ensureTestDeps, contrib" description="Sets up the test so that 'ant test' will test against OpenJDK6."> <mkdir dir="lib/openJDK6Environment" /> <get src="http://projectlombok.org/ivyrepo/langtools/javac-1.6.0.18.jar" dest="lib/openJDK6Environment/javac6.jar" verbose="true" usetimestamp="true" /> <get src="http://projectlombok.org/ivyrepo/langtools/rt-openjdk6.jar" dest="lib/openJDK6Environment/rt-openjdk6.jar" verbose="true" usetimestamp="true" /> @@ -475,7 +475,7 @@ ${sourceWarning}</echo> <antcall target="-createEclipseLaunchForTestEnvironmentIfEclipseProject" /> </target> - <target name="setupJavaOpenJDK7TestEnvironment" depends="contrib" description="Sets up the test so that 'ant test' will test against OpenJDK7."> + <target name="setupJavaOpenJDK7TestEnvironment" depends="ensureTestDeps, contrib" description="Sets up the test so that 'ant test' will test against OpenJDK7."> <mkdir dir="lib/openJDK7Environment" /> <get src="http://projectlombok.org/ivyrepo/langtools/javac-1.7.0.jar" dest="lib/openJDK7Environment/javac7.jar" verbose="true" usetimestamp="true" /> <get src="http://projectlombok.org/ivyrepo/langtools/rt-openjdk7.jar" dest="lib/openJDK7Environment/rt-openjdk7.jar" verbose="true" usetimestamp="true" /> @@ -490,7 +490,7 @@ ${sourceWarning}</echo> <antcall target="-createEclipseLaunchForTestEnvironmentIfEclipseProject" /> </target> - <target name="setupJavaOracle7TestEnvironment" depends="contrib" description="Sets up the test so that 'ant test' will test against OpenJDK7."> + <target name="setupJavaOracle7TestEnvironment" depends="ensureTestDeps, contrib" description="Sets up the test so that 'ant test' will test against OpenJDK7."> <mkdir dir="lib/oracleJDK7Environment" /> <get src="http://projectlombok.org/ivyrepo/langtools/oracle-jdk7-tools.jar" dest="lib/oracleJDK7Environment/tools.jar" verbose="true" usetimestamp="true" /> <get src="http://projectlombok.org/ivyrepo/langtools/oracle-jdk7-rt.jar" dest="lib/oracleJDK7Environment/rt.jar" verbose="true" usetimestamp="true" /> @@ -505,7 +505,7 @@ ${sourceWarning}</echo> <antcall target="-createEclipseLaunchForTestEnvironmentIfEclipseProject" /> </target> - <target name="setupJavaOracle8TestEnvironment" depends="contrib" description="Sets up the test so that 'ant test' will test against OpenJDK8."> + <target name="setupJavaOracle8TestEnvironment" depends="ensureTestDeps, contrib" description="Sets up the test so that 'ant test' will test against OpenJDK8."> <mkdir dir="lib/oracleJDK8Environment" /> <get src="http://projectlombok.org/ivyrepo/langtools/jdk8-javac.jar" dest="lib/oracleJDK8Environment/javac8.jar" verbose="true" usetimestamp="true" /> <get src="http://projectlombok.org/ivyrepo/langtools/oracle-jdk8-rt.jar" dest="lib/oracleJDK8Environment/rt.jar" verbose="true" usetimestamp="true" /> diff --git a/buildScripts/eclipse-run-tests.template b/buildScripts/eclipse-run-tests.template index a38283dc..7c82c7d8 100644 --- a/buildScripts/eclipse-run-tests.template +++ b/buildScripts/eclipse-run-tests.template @@ -19,6 +19,7 @@ <listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/lombok/@ECJ_LOCATION@" path="3" type="2"/> "/> <listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/lombok/@JAVAC_LOCATION@" path="3" type="2"/> "/> <listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/lombok/lib/test/com.google.guava-guava.jar" path="3" type="2"/> "/> + <listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry internalArchive="/lombok/lib/test/com.google.code.findbugs-findbugs.jar" path="3" type="2"/> "/> <listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath"> <memento exportedEntriesOnly="false" project="lombok"/> </runtimeClasspathEntry> "/> </listAttribute> <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/> diff --git a/buildScripts/ivy.xml b/buildScripts/ivy.xml index 5d362aab..61fbbe2d 100644 --- a/buildScripts/ivy.xml +++ b/buildScripts/ivy.xml @@ -25,7 +25,7 @@ <dependency org="org.slf4j" name="slf4j-api" rev="1.6.1" conf="test->default; contrib->sources"/> <dependency org="org.slf4j" name="slf4j-ext" rev="1.6.1" conf="test->default; contrib->sources"/> <dependency org="com.google.guava" name="guava" rev="18.0" conf="test->default; contrib->sources" /> - <dependency org="com.googlecode.jarjar" name="jarjar" rev="1.1" conf="buildBase->default" /> + <dependency org="com.google.code.findbugs" name="findbugs" rev="3.0.0" conf="test->master" /> <dependency org="org.apache.ant" name="ant" rev="1.8.1" conf="buildBase->default; contrib->sources" /> <dependency org="projectlombok.org" name="spi" rev="0.2.7" conf="buildBase->build" /> diff --git a/doc/changelog.markdown b/doc/changelog.markdown index 5392927f..4243a9be 100644 --- a/doc/changelog.markdown +++ b/doc/changelog.markdown @@ -4,6 +4,7 @@ Lombok Changelog ### v1.16.1 "Edgy Guinea Pig" * BUGFIX: The ant `delombok` task was broken starting with v1.16.0. Note that the task def class has been changed; taskdef `lombok.delombok.ant.Tasks$Delombok` instead of the old `lombok.delombok.ant.DelombokTask`. [Issue #775](https://code.google.com/p/projectlombok/issues/detail?id=775). * BUGFIX: `val` in javac would occasionally fail if used inside inner classes. This is (probably) fixed. [Issue #694](https://code.google.com/p/projectlombok/issues/detail?id=694). +* FEATURE: The config key `lombok.extern.findbugs.addSuppressFBWarnings` can now be used to add findbugs suppress warnings annotations to all code lombok generates. This addresses feature request [Issue #702](https://code.google.com/p/projectlombok/issues/detail?id=702). ### v1.16.0 "Candid Duck" (January 26th, 2015) * BUGFIX: `@ExtensionMethod` was broken in Eclipse using java 8. [Issue #742](https://code.google.com/p/projectlombok/issues/detail?id=742), [Issue #747](https://code.google.com/p/projectlombok/issues/detail?id=747) diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index e18fe66c..3e2330fd 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -44,6 +44,15 @@ public class ConfigurationKeys { */ public static final ConfigurationKey<Boolean> ADD_GENERATED_ANNOTATIONS = new ConfigurationKey<Boolean>("lombok.addGeneratedAnnotation", "Generate @javax.annotation.Generated on all generated code (default: true).") {}; + /** + * lombok configuration: {@code lombok.extern.findbugs.addSuppressFBWarnings} = {@code true} | {@code false}. + * + * If {@code true}, lombok generates {@code edu.umd.cs.findbugs.annotations.SuppressFBWarnings} on all fields, methods, and types that are generated. + * + * NB: If you enable this option, findbugs must be on the source or classpath, or you'll get errors that the type {@code SuppressFBWarnings} cannot be found. + */ + public static final ConfigurationKey<Boolean> ADD_FINDBUGS_SUPPRESSWARNINGS_ANNOTATIONS = new ConfigurationKey<Boolean>("lombok.extern.findbugs.addSuppressFBWarnings", "Generate @edu.umd.cs.findbugs.annotations.SuppressFBWArnings on all generated code (default: false).") {}; + // ----- *ArgsConstructor ----- /** diff --git a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java index 23a5f4bc..836a51c6 100644 --- a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java +++ b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java @@ -1215,7 +1215,7 @@ public class EclipseHandlerUtil { * The field carries the @{@link SuppressWarnings}("all") annotation. */ public static EclipseNode injectFieldAndMarkGenerated(EclipseNode type, FieldDeclaration field) { - field.annotations = addSuppressWarningsAll(field, field.annotations); + field.annotations = addSuppressWarningsAll(type, field, field.annotations); field.annotations = addGenerated(type, field, field.annotations); return injectField(type, field); } @@ -1261,7 +1261,7 @@ public class EclipseHandlerUtil { * Inserts a method into an existing type. The type must represent a {@code TypeDeclaration}. */ public static EclipseNode injectMethod(EclipseNode type, AbstractMethodDeclaration method) { - method.annotations = addSuppressWarningsAll(method, method.annotations); + method.annotations = addSuppressWarningsAll(type, method, method.annotations); method.annotations = addGenerated(type, method, method.annotations); TypeDeclaration parent = (TypeDeclaration) type.get(); @@ -1304,7 +1304,7 @@ public class EclipseHandlerUtil { * @param type New type (class, interface, etc) to inject. */ public static EclipseNode injectType(final EclipseNode typeNode, final TypeDeclaration type) { - type.annotations = addSuppressWarningsAll(type, type.annotations); + type.annotations = addSuppressWarningsAll(typeNode, type, type.annotations); type.annotations = addGenerated(typeNode, type, type.annotations); TypeDeclaration parent = (TypeDeclaration) typeNode.get(); @@ -1321,11 +1321,21 @@ public class EclipseHandlerUtil { } private static final char[] ALL = "all".toCharArray(); + private static final char[] JUSTIFICATION = "justification".toCharArray(); + private static final char[] GENERATED_CODE = "generated code".toCharArray(); private static final char[] LOMBOK = "lombok".toCharArray(); private static final char[][] JAVAX_ANNOTATION_GENERATED = Eclipse.fromQualifiedName("javax.annotation.Generated"); + private static final char[][] EDU_UMD_CS_FINDBUGS_ANNOTATIONS_SUPPRESSFBWARNINGS = Eclipse.fromQualifiedName("edu.umd.cs.findbugs.annotations.SuppressFBWarnings"); - public static Annotation[] addSuppressWarningsAll(ASTNode source, Annotation[] originalAnnotationArray) { - return addAnnotation(source, originalAnnotationArray, TypeConstants.JAVA_LANG_SUPPRESSWARNINGS, new StringLiteral(ALL, 0, 0, 0)); + public static Annotation[] addSuppressWarningsAll(EclipseNode node, ASTNode source, Annotation[] originalAnnotationArray) { + Annotation[] anns = addAnnotation(source, originalAnnotationArray, TypeConstants.JAVA_LANG_SUPPRESSWARNINGS, new StringLiteral(ALL, 0, 0, 0)); + + if (Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_FINDBUGS_SUPPRESSWARNINGS_ANNOTATIONS))) { + MemberValuePair mvp = new MemberValuePair(JUSTIFICATION, 0, 0, new StringLiteral(GENERATED_CODE, 0, 0, 0)); + anns = addAnnotation(source, anns, EDU_UMD_CS_FINDBUGS_ANNOTATIONS_SUPPRESSFBWARNINGS, mvp); + } + + return anns; } public static Annotation[] addGenerated(EclipseNode node, ASTNode source, Annotation[] originalAnnotationArray) { @@ -1333,7 +1343,7 @@ public class EclipseHandlerUtil { return addAnnotation(source, originalAnnotationArray, JAVAX_ANNOTATION_GENERATED, new StringLiteral(LOMBOK, 0, 0, 0)); } - private static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotationArray, char[][] annotationTypeFqn, Expression arg) { + private static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotationArray, char[][] annotationTypeFqn, ASTNode arg) { char[] simpleName = annotationTypeFqn[annotationTypeFqn.length - 1]; if (originalAnnotationArray != null) for (Annotation ann : originalAnnotationArray) { @@ -1353,15 +1363,32 @@ public class EclipseHandlerUtil { long p = (long)pS << 32 | pE; long[] poss = new long[annotationTypeFqn.length]; Arrays.fill(poss, p); - QualifiedTypeReference suppressWarningsType = new QualifiedTypeReference(annotationTypeFqn, poss); - setGeneratedBy(suppressWarningsType, source); - SingleMemberAnnotation ann = new SingleMemberAnnotation(suppressWarningsType, pS); - ann.declarationSourceEnd = pE; - if (arg != null) { + QualifiedTypeReference qualifiedType = new QualifiedTypeReference(annotationTypeFqn, poss); + setGeneratedBy(qualifiedType, source); + Annotation ann; + if (arg instanceof Expression) { + SingleMemberAnnotation sma = new SingleMemberAnnotation(qualifiedType, pS); + sma.declarationSourceEnd = pE; arg.sourceStart = pS; arg.sourceEnd = pE; - ann.memberValue = arg; - setGeneratedBy(ann.memberValue, source); + sma.memberValue = (Expression) arg; + setGeneratedBy(sma.memberValue, source); + ann = sma; + } else if (arg instanceof MemberValuePair) { + NormalAnnotation na = new NormalAnnotation(qualifiedType, pS); + na.declarationSourceEnd = pE; + arg.sourceStart = pS; + arg.sourceEnd = pE; + na.memberValuePairs = new MemberValuePair[] {(MemberValuePair) arg}; + setGeneratedBy(na.memberValuePairs[0], source); + setGeneratedBy(na.memberValuePairs[0].value, source); + na.memberValuePairs[0].value.sourceStart = pS; + na.memberValuePairs[0].value.sourceEnd = pE; + ann = na; + } else { + MarkerAnnotation ma = new MarkerAnnotation(qualifiedType, pS); + ma.declarationSourceEnd = pE; + ann = ma; } setGeneratedBy(ann, source); if (originalAnnotationArray == null) return new Annotation[] { ann }; diff --git a/src/core/lombok/javac/handlers/JavacHandlerUtil.java b/src/core/lombok/javac/handlers/JavacHandlerUtil.java index 599a4753..a073ac0d 100644 --- a/src/core/lombok/javac/handlers/JavacHandlerUtil.java +++ b/src/core/lombok/javac/handlers/JavacHandlerUtil.java @@ -953,12 +953,20 @@ public class JavacHandlerUtil { public static void addSuppressWarningsAll(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context) { if (!LombokOptionsFactory.getDelombokOptions(context).getFormatPreferences().generateSuppressWarnings()) return; addAnnotation(mods, node, pos, source, context, "java.lang.SuppressWarnings", node.getTreeMaker().Literal("all")); + + if (Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_FINDBUGS_SUPPRESSWARNINGS_ANNOTATIONS))) { + JavacTreeMaker maker = node.getTreeMaker(); + JCExpression arg = maker.Assign(maker.Ident(node.toName("justification")), maker.Literal("generated code")); + addAnnotation(mods, node, pos, source, context, "edu.umd.cs.findbugs.annotations.SuppressFBWarnings", arg); + } } public static void addGenerated(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context) { - if (Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_GENERATED_ANNOTATIONS))) return; if (!LombokOptionsFactory.getDelombokOptions(context).getFormatPreferences().generateGenerated()) return; - addAnnotation(mods, node, pos, source, context, "javax.annotation.Generated", node.getTreeMaker().Literal("lombok")); + + if (!Boolean.FALSE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_GENERATED_ANNOTATIONS))) { + addAnnotation(mods, node, pos, source, context, "javax.annotation.Generated", node.getTreeMaker().Literal("lombok")); + } } private static void addAnnotation(JCModifiers mods, JavacNode node, int pos, JCTree source, Context context, String annotationTypeFqn, JCExpression arg) { @@ -981,7 +989,13 @@ public class JavacHandlerUtil { JavacTreeMaker maker = node.getTreeMaker(); JCExpression annType = isJavaLangBased ? genJavaLangTypeRef(node, simpleName) : chainDotsString(node, annotationTypeFqn); annType.pos = pos; - if (arg != null) arg.pos = pos; + if (arg != null) { + arg.pos = pos; + if (arg instanceof JCAssign) { + ((JCAssign) arg).lhs.pos = pos; + ((JCAssign) arg).rhs.pos = pos; + } + } List<JCExpression> argList = arg != null ? List.of(arg) : List.<JCExpression>nil(); JCAnnotation annotation = recursiveSetGeneratedBy(maker.Annotation(annType, argList), source, context); annotation.pos = pos; diff --git a/test/core/src/lombok/RunTestsViaEcj.java b/test/core/src/lombok/RunTestsViaEcj.java index 1571f401..272ed8b7 100644 --- a/test/core/src/lombok/RunTestsViaEcj.java +++ b/test/core/src/lombok/RunTestsViaEcj.java @@ -143,6 +143,7 @@ public class RunTestsViaEcj extends AbstractRunTests { classpath.add("lib/test/log4j-log4j.jar"); classpath.add("lib/test/org.apache.logging.log4j-log4j-api.jar"); classpath.add("lib/test/com.google.guava-guava.jar"); + classpath.add("lib/test/com.google.code.findbugs-findbugs.jar"); return new FileSystem(classpath.toArray(new String[0]), new String[] {file.getAbsolutePath()}, "UTF-8"); } } diff --git a/test/transform/resource/after-delombok/GenerateSuppressFBWarnings.java b/test/transform/resource/after-delombok/GenerateSuppressFBWarnings.java new file mode 100644 index 00000000..377145e4 --- /dev/null +++ b/test/transform/resource/after-delombok/GenerateSuppressFBWarnings.java @@ -0,0 +1,9 @@ +class GenerateSuppressFBWarnings { + int y; + @java.lang.SuppressWarnings("all") + @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code") + @javax.annotation.Generated("lombok") + public int getY() { + return this.y; + } +}
\ No newline at end of file diff --git a/test/transform/resource/after-ecj/GenerateSuppressFBWarnings.java b/test/transform/resource/after-ecj/GenerateSuppressFBWarnings.java new file mode 100644 index 00000000..407d41b6 --- /dev/null +++ b/test/transform/resource/after-ecj/GenerateSuppressFBWarnings.java @@ -0,0 +1,9 @@ +class GenerateSuppressFBWarnings { + @lombok.Getter int y; + GenerateSuppressFBWarnings() { + super(); + } + public @java.lang.SuppressWarnings("all") @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code") @javax.annotation.Generated("lombok") int getY() { + return this.y; + } +}
\ No newline at end of file diff --git a/test/transform/resource/before/GenerateSuppressFBWarnings.java b/test/transform/resource/before/GenerateSuppressFBWarnings.java new file mode 100644 index 00000000..2ab9c744 --- /dev/null +++ b/test/transform/resource/before/GenerateSuppressFBWarnings.java @@ -0,0 +1,5 @@ +//VERSION 7: +//CONF: lombok.extern.findbugs.addSuppressFBWarnings = true +class GenerateSuppressFBWarnings { + @lombok.Getter int y; +} diff --git a/website/features/configuration.html b/website/features/configuration.html index 1f31fe01..7e79a9c0 100644 --- a/website/features/configuration.html +++ b/website/features/configuration.html @@ -21,7 +21,9 @@ Usually, a user of lombok puts a <code>lombok.config</code> file with their preferences in a workspace or project root directory, with the special <code>config.stopBubbling = true</code> key to tell lombok this is your root directory. You can then create <code>lombok.config</code> files in any subdirectories (generally representing projects or source packages) with different settings. </p><p> An up to date list of all configuration keys supported by your version of lombok can be generated by running: - <div class="snippet example"><code>java -jar lombok.jar config -g --verbose</code></div> + <div class="snippet example"> + <code>java -jar lombok.jar config -g --verbose</code> + </div> The output of the <em>config</em> tool is itself a valid <code>lombok.config</code> file.<br /> The <em>config</em> tool can also be used to display the complete lombok configuration used for any given directory or source file by supplying these as arguments. </p><p> @@ -42,7 +44,7 @@ Configuration files are hierarchical: Any configuration setting applies to all source files in that directory, and all source files in subdirectories, but configuration settings closer to the source file take precedence. For example, if you have in <code>/Users/me/projects/lombok.config</code> the following: <div class="snippet example"> <code>lombok.log.fieldName = foobar</code> - </div></div> + </div> and in <code>/Users/me/projects/MyProject/lombok.config</code> you have: <div class="snippet example"> <code>lombok.log.fieldName = xyzzy</code> @@ -61,14 +63,27 @@ <code>lombok.accessors.prefix += m_</code> </div> </p><p> + Comments can be included in <code>lombok.config</code> files; any line that starts with <code>#</code> is considered a comment. + </p> + </div> + <div class="overview" style="clear: left;"> + <h3>Global config keys</h3> + <p> To stop lombok from looking at parent directories for more configuration files, the special key: <div class="snippet example"> <code>config.stopBubbling = true</code> </div> - can be included. + can be included. We suggest you put this in the root of your workspace directory. </p><p> - Comments can be included in <code>lombok.config</code> files; any line that starts with <code>#</code> is considered a comment. - </p> + Lombok normally adds <code>@javax.annotation.Generated</code> annotations to all generated nodes where possible. You can stop this with: + <div class="snippet example"> + <code>lombok.addGeneratedAnnotation = false</code> + </div> + </p><p> + Lombok can add the <code>@SuppressFBWarnings</code> annotation which is useful if you want to run <a href="http://findbugs.sourceforge.net/">FindBugs</a> on your class files. To enable this feature, make sure findbugs is on the classpath when you compile, and add the following config key: + <div class="snippet example"> + <code>lombok.extern.findbugs.addSuppressFBWarnings = true</code> + </div> </div> <div style="clear: left;"></div> <div class="footer"> |