/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* LECCJavaSourceGenerator.java
* Creation date: Oct 18, 2006.
* By: Edward Lam
*/
package org.openquark.cal.internal.machine.lecc;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.openquark.cal.compiler.CompilerMessageLogger;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.TypeConstructor;
import org.openquark.cal.internal.javamodel.BytecodeDebuggingUtilities;
import org.openquark.cal.internal.javamodel.JavaClassRep;
import org.openquark.cal.internal.javamodel.JavaGenerationException;
import org.openquark.cal.internal.javamodel.JavaSourceGenerator;
import org.openquark.cal.internal.javamodel.JavaStatement.JavaDocComment;
import org.openquark.cal.internal.machine.CodeGenerationException;
import org.openquark.cal.internal.machine.lecc.LECCModule.FunctionGroupInfo;
import org.openquark.cal.internal.runtime.lecc.LECCMachineConfiguration;
import org.openquark.cal.machine.ProgramResourceLocator;
import org.openquark.cal.machine.ProgramResourceRepository;
import org.openquark.cal.machine.StatusListener;
import org.openquark.cal.services.ResourcePath;
import org.openquark.util.FileSystemHelper;
import org.openquark.util.TextEncodingUtilities;
/**
* @author Edward Lam, Raymond Cypher
*/
public class LECCJavaSourceGenerator extends JavaGenerator {
/**
* Calls a helper method which performs various checks on the generated bytecode (i.e. the bytecode produced by
* javac from compiling the generated java source). This is mostly of use when comparing the bytecode produced
* by javac with the bytecode produced by our direct bytecode generators.
* Individual tests and debug output can be turned on by setting the boolean variables in the debugGeneratedBytecode method below.
* Note that these tests slow down bytecode generation considerably.
*/
private static final boolean DEBUG_GENERATED_BYTECODE = false;
/** The method object used to invoke the javac compiler on the generated sources. */
private static Method compileMethod;
static {
// Initialize the compileMethod field.
// First we need to get the compiler class. i.e. com.sun.tools.javac.Main
Class<?> compilerClass = null;
// Start by trying to get the class from the current class loader.
try {
compilerClass = JavaSourceGenerator.class.getClassLoader().loadClass ("com.sun.tools.javac.Main");
} catch (ClassNotFoundException e) {
// The class is not on the current classpath. Try to locate tools.jar based on the jre home
// directory and use a URLClassLoader.
String homeDirectory = System.getProperty("java.home");
if (homeDirectory != null) {
// In the standard JDC file layout tools.jar will be in the lib directory that is at the
// same level as the JRE home directory.
String fileSeparator = System.getProperty("file.separator", "\\");
// Trim the 'jre' off the home directory.
homeDirectory = homeDirectory.substring(0, homeDirectory.lastIndexOf(fileSeparator));
// Create the file for tools.jar and check if it exists.
File toolsFile = new File(homeDirectory + fileSeparator + "lib" + fileSeparator + "tools.jar");
if (FileSystemHelper.fileExists(toolsFile)) {
try {
// Since we've located tools.jar create a URLClassLoader with tools.jar as its path and
// attempt to load the Main class.
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{toolsFile.toURL()}, JavaSourceGenerator.class.getClassLoader());
compilerClass = urlClassLoader.loadClass("com.sun.tools.javac.Main");
} catch (MalformedURLException e2) {
// Simply fall through leaving compilerClass as null.
} catch (ClassNotFoundException e2) {
// Simply fall through leaving compilerClass as null.
}
}
}
}
if (compilerClass != null) {
try {
// First we try to get the version of compile that takes a PrintWriter to handle output messages.
LECCJavaSourceGenerator.compileMethod = compilerClass.getMethod("compile", new Class[]{(new String[]{}).getClass(), PrintWriter.class});
} catch (NoSuchMethodException e) {
// Simply fall through and leave compileMethod as null.
}
}
}
/**
* If javac returns an error when trying to compile generated sources for a module we want to delete all
* generated files. Otherwise we can get into a state where we assume that an existing java source and
* the corresponding class file are synchronized when they're actually not.
* This flag allows the behaviour to be turned off for debugging purposes. i.e. to examine the generated
* source that failed to compile.
*/
private static final boolean DELETE_GENERATED_SOURCES_ON_COMPILATION_ERROR = true;
private static final String EOL = System.getProperty("line.separator");
private final List<ProgramResourceLocator.File> filesToCompile = new ArrayList<ProgramResourceLocator.File>();
/** The folder in which the java resources for the module should exist. */
private final ProgramResourceLocator.Folder moduleFolder;
/** The repository for program resources. */
private final ProgramResourceRepository resourceRepository;
/** (Set of ModuleName) The names of dependee modules (including the current module) in the program.
* This is needed for setting the path used by the java compiler. */
private final Set<ModuleName> dependeeModuleNames;
private final LECCModule module;
/**
* Emit an indent.
* @param sb the StringBuilder to which to add an indent.
* @param indent the number of indents to add.
* @return StringBuilder sb, returned for convenience.
*/
private static StringBuilder emitIndent(StringBuilder sb, int indent) {
// NOTE: we use the tab character '\t' instead of adding spaces
// because of memory issues.
// When using spaces instead of '\t' some of our generated java source
// files were large enough to cause out-of-memory errors.
for (int i = 0; i < indent; i++) {
sb.append('\t');
}
return sb;
}
/**
* Emit an indent, some text, and an EOL.
* @param sb the StringBuilder to which to add an indent.
* @param text the text to add.
* @param indent the number of indents to add.
*/
private static void emitLine(StringBuilder sb, int indent,String text) {
emitIndent(sb, indent);
sb.append(text + EOL);
}
/**
* Constructor for an LECCJavaSourceGenerator
* @param module The module for which java classes will be generated.
* @param resourceRepository The repository for program resources.
* @param dependeeModuleNames - Set of String. The names of dependee modules, including the current module.
* @throws IOException if there was a problem creating the repository folder where the source generation files will exist.
*/
LECCJavaSourceGenerator(LECCModule module, ProgramResourceRepository resourceRepository, Set<ModuleName> dependeeModuleNames) throws IOException {
this.resourceRepository = resourceRepository;
if (module == null || dependeeModuleNames == null) {
throw new IllegalArgumentException ("Unable to create JavaSourceGenerator: null argument.");
}
this.module = module;
this.dependeeModuleNames = dependeeModuleNames;
this.moduleFolder = CodeGenerator.getModuleResourceFolder(module.getName());
// Ensure the module repository folder exists.
resourceRepository.ensureFolderExists(moduleFolder);
}
/** {@inheritDoc} */
@Override
void createFunction(FunctionGroupInfo functionGroupInfo, boolean forceWrite, CompilerMessageLogger logger)
throws CodeGenerationException {
String className = CALToJavaNames.createClassNameFromSC(functionGroupInfo.getFunctionGroupQualifiedName(), module);
ProgramResourceLocator.File sourceFile = moduleFolder.extendFile(className + ".java");
ProgramResourceLocator.File classFile = moduleFolder.extendFile(className + ".class");
//System.out.println(functionGroupInfo.getFunctionGroupName());
boolean sourceExists = resourceRepository.exists(sourceFile);
boolean fileChange = false;
if (forceWrite || !sourceExists) {
// Get the sc definition, generate source.
JavaClassRep classRep = JavaDefinitionBuilder.getSCDefinition(functionGroupInfo, module, getCodeGenerationStats());
if (classRep.getJavaDoc() == null) {
classRep.setJavaDoc(new JavaDocComment(getClassJavadocComment()));
}
try {
String source =
JavaSourceGenerator.generateSourceCode(classRep);
if (!sourceExists) {
writeToSourceFile (source, sourceFile);
filesToCompile.add(sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
fileChange = true;
} else
if (sourceChanged (source, sourceFile)) {
writeToSourceFile (source, sourceFile);
filesToCompile.add(sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
fileChange = true;
} else
if (!resourceRepository.exists(classFile)) {
filesToCompile.add(sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
fileChange = true;
}
} catch (JavaGenerationException e) {
throw new CodeGenerationException(e.getLocalizedMessage(), e);
}
} else {
if (!resourceRepository.exists(classFile)) {
filesToCompile.add(sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
fileChange = true;
}
}
informStatusListeners(fileChange ? StatusListener.SM_ENTITY_GENERATED_FILE_WRITTEN : StatusListener.SM_ENTITY_GENERATED, functionGroupInfo.getFunctionGroupName());
}
/** {@inheritDoc} */
@Override
void createTypeDefinition(TypeConstructor typeCons,
boolean forceWrite, CompilerMessageLogger logger) throws CodeGenerationException {
String javaTypeConsName = CALToJavaNames.createClassNameFromType(typeCons, module);
ProgramResourceLocator.File sourceFile = moduleFolder.extendFile(javaTypeConsName + ".java");
ProgramResourceLocator.File classFile = moduleFolder.extendFile(javaTypeConsName + ".class");
boolean sourceExists = resourceRepository.exists(sourceFile);
boolean fileChange = false;
if (forceWrite || !sourceExists) {
// Get the sc definition, generate source.
JavaClassRep classRep = JavaDefinitionBuilder.getDataTypeDefinition(typeCons, module, getCodeGenerationStats());
if (classRep.getJavaDoc() == null) {
classRep.setJavaDoc(new JavaDocComment(getClassJavadocComment()));
}
try {
String source =
JavaSourceGenerator.generateSourceCode(classRep);
if (!sourceExists) {
writeToSourceFile (source, sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
filesToCompile.add(sourceFile);
fileChange = true;
} else
if (sourceChanged (source, sourceFile)) {
writeToSourceFile (source, sourceFile);
//System.out.println ("adding to compile: " + sourceFile.getName());
filesToCompile.add(sourceFile);
fileChange = true;
} else
if (!resourceRepository.exists(classFile)) {
//System.out.println ("adding to compile: " + sourceFile.getName());
filesToCompile.add(sourceFile);
fileChange = true;
}
} catch (JavaGenerationException e) {
throw new CodeGenerationException(e.getLocalizedMessage(), e);
}
} else {
if (!resourceRepository.exists(classFile)) {
//System.out.println ("adding to compile: " + sourceFile.getName());
filesToCompile.add(sourceFile);
fileChange = true;
}
}
informStatusListeners(fileChange ? StatusListener.SM_ENTITY_GENERATED_FILE_WRITTEN : StatusListener.SM_ENTITY_GENERATED, typeCons.getName().getUnqualifiedName());
}
/** {@inheritDoc} */
@Override
void wrap() throws CodeGenerationException {
if (compileMethod == null) {
throw new CodeGenerationException ("Error compiling generated source code: Unable to locate or access class com.sun.tools.javac.Main. Generating Java source requires an installed JDK. Please ensure that tools.jar is on the classpath.");
}
if (filesToCompile.isEmpty()) {
return;
}
try {
// Let any status listeners know that we're starting the compilation of java sources for this module.
informStatusListeners(StatusListener.SM_START_COMPILING_GENERATED_SOURCE, module.getName());
// Set up the classpath for compiling this module.
String fileNames[] = new String [filesToCompile.size()];
{
int index = 0;
for (final ProgramResourceLocator.File fileToCompile : filesToCompile) {
File file = resourceRepository.getFile(fileToCompile);
if (file == null) {
// the fileToCompile refers to a resource that is not backed by the file system (e.g. in a Car)
// We do not support Java source generation for modules whose source comes from Car files.
throw new CodeGenerationException ("Error compiling generated source code: The location of the source file " + fileToCompile + " does not have a corresponding java.io.File representation (e.g. the module comes from a Car).");
}
fileNames[index] = file.getPath();
index++;
}
}
String currentPath = System.getProperty("java.class.path", ".");
String pathSeparator = System.getProperty ("path.separator", ";");
String rootPackagePathSegment = LECCMachineConfiguration.ROOT_PACKAGE.replace('.', File.separatorChar);
Set<String> pathAdditions = new HashSet<String>();
for (final ModuleName moduleName : dependeeModuleNames) {
// Get module, add its base folder directory to the classpath.
ProgramResourceLocator.Folder moduleResourceFolder = new ProgramResourceLocator.Folder(moduleName, ResourcePath.EMPTY_PATH);
File moduleDir = resourceRepository.getFile(moduleResourceFolder);
if (moduleDir == null) {
// the fileToCompile refers to a resource that is not backed by the file system (e.g. in a Car)
// We do not support Java source generation for modules whose source comes from Car files.
throw new CodeGenerationException ("Error compiling generated source code: The location of the module directory " + moduleResourceFolder + " does not have a corresponding java.io.File representation (e.g. the module comes from a Car).");
}
String path = moduleDir.getAbsolutePath();
int index = path.indexOf(rootPackagePathSegment);
if (index >= 0) {
path = path.substring(0, index);
}
pathAdditions.add(path);
}
for (final String pathAddition : pathAdditions) {
currentPath += pathSeparator + pathAddition;
}
// Set up the classpath arguments for the call to 'compile'.
String args[] = new String [fileNames.length + 6];
args[0] = "-target";
args[1] = "1.5";
args[2] = "-source";
args[3] = "1.5";
args[4] = "-classpath";
args[5] = currentPath;
System.arraycopy(fileNames, 0, args, 6, fileNames.length);
// Set up a PrintWriter that will write any status messages from the compiler into a string buffer.
FixedSizeStringWriter sw = new FixedSizeStringWriter(1000);
PrintWriter pw = new PrintWriter(sw);
// Invoke the compiler.
Object compileResult = compileMethod.invoke(null, new Object[]{args, pw});
// Close/flush the PrintWriter.
pw.close();
// Extract the integer return code from the object result of 'invoke'.
int returnCode = ((Integer)compileResult).intValue();
if (returnCode != 0) {
if (DELETE_GENERATED_SOURCES_ON_COMPILATION_ERROR) {
// We need to clean out the generated source target directory. Otherwise we can get into a state where
// we assume that an existing java source and the corresponding class file are synchronized when they're
// actually not.
try {
resourceRepository.delete(moduleFolder);
// TODO: actually handle this.
} catch (IOException ioe) {
// There was a problem deleting one or more files.
// This used to be just ignored.
System.err.println("Problem deleting one or more files: " + ioe);
}
}
// Construct an error message that includes any messages from the javac compiler.
throw new CodeGenerationException ("Error compiling generated source for module " + module.getName() + ".\n" + sw.toString());
}
if (DEBUG_GENERATED_BYTECODE) {
debugGeneratedBytecode();
}
} catch (IllegalAccessException e) {
throw new CodeGenerationException ("Error compiling generated source code: IllegalAccessException thrown trying to access com.sun.tools.javac.Main.compile.");
} catch (InvocationTargetException e) {
throw new CodeGenerationException ("Error compiling generated source code: InvocationTargetException thrown trying to access com.sun.tools.javac.Main.compile. " + e.getLocalizedMessage());
} finally {
// Inform any status listeners that compilation of java source for this module is ended.
informStatusListeners(StatusListener.SM_END_COMPILING_GENERATED_SOURCE, module.getName());
}
}
/**
* Get a string to represent the javadoc comment for the generated class.
* @return the javadoc comment.
*/
private String getClassJavadocComment() {
File file = resourceRepository.getFile(moduleFolder);
String fileName;
if (file != null) {
fileName = file.toString().replaceAll("\\\\", "/");
} else {
fileName = moduleFolder.toString();
}
StringBuilder sb = new StringBuilder();
emitLine(sb, 0, "/**");
emitLine(sb, 0, " * " + fileName);
emitLine(sb, 0, " * from CAL module \"" + module.getName() + "\"");
emitLine(sb, 0, " * created at " + new Date());
emitLine(sb, 0, " * " + module.getNFunctions() + " CAL symbols defined (supercombinators and constructors) in this module");
emitLine(sb, 0, " *");
emitLine(sb, 0, " * This Java source has been automatically generated by the Business Objects CAL Compiler");
emitLine(sb, 0, " * MODIFICATIONS TO THIS SOURCE MAY BE OVERWRITTEN - DO NOT MODIFY THIS FILE");
emitLine(sb, 0, " */");
return sb.toString();
}
/**
* Write the given source into the given file.
* @param source - String. The generated source.
* @param targetFile - File. The target file.
* @throws CodeGenerationException
*/
private void writeToSourceFile(String source, ProgramResourceLocator.File targetFile) throws CodeGenerationException {
//System.out.println ("writing source file: " + file.getName());
try {
resourceRepository.setContents(targetFile, new ByteArrayInputStream(TextEncodingUtilities.getUTF8Bytes(source)));
} catch (FileNotFoundException e) {
throw new CodeGenerationException ("Unable to find file: " + targetFile.toString());
} catch (IOException e) {
throw new CodeGenerationException ("Error writing to file: " + targetFile.toString());
}
}
/**
* Determine whether the source code for a given file has changed in a meaningful way.
* @param newSource the new source code for the give file.
* @param sourceFile the source file.
* @return boolean whether the source code has changed.
*/
private boolean sourceChanged(String newSource, ProgramResourceLocator.File sourceFile) {
if (!resourceRepository.exists(sourceFile)) {
return true;
}
InputStream sourceInputStream = null;
try {
sourceInputStream = resourceRepository.getContents(sourceFile);
Reader frOld = TextEncodingUtilities.makeUTF8Reader(sourceInputStream);
BufferedReader brOld = new BufferedReader (frOld);
BufferedReader brNew = new BufferedReader (new StringReader (newSource));
boolean difference = false;
while (!difference) {
String sOld = nextLineOfInterest(brOld);
String sNew = nextLineOfInterest(brNew);
if ((sOld == null && sNew != null) || (sNew == null && sOld != null)) {
return true;
}
if (sOld == null) {
break;
}
if (!sOld.equals (sNew)) {
return true;
}
}
brOld.close();
frOld.close();
} catch (IOException e) {
return true;
} finally {
if (sourceInputStream != null) {
try {
sourceInputStream.close();
} catch (IOException e) {
}
}
}
return false;
}
/**
* Helper method for sourceChanged().
* Get the next line which isn't a comment.
* @param br the reader from which to get the line.
* @return the next line, or null if there is no next line.
* @throws IOException
*/
private String nextLineOfInterest (BufferedReader br) throws IOException {
String line = null;
boolean readAgain = true;
boolean inComment = false;
while (readAgain) {
readAgain = false;
line = br.readLine ();
if (line == null) {
return null;
}
line = line.trim();
if (inComment) {
readAgain = true;
if (line.endsWith("*/")) {
inComment = false;
}
} else
if (line.startsWith("/*")) {
inComment = true;
readAgain = true;
} else
if (line.equals("")) {
readAgain = true;
} else
if (line.startsWith("//")) {
readAgain = true;
}
}
return line;
}
/**
* Called if the static flag JavaSourceGenerator.DEBUG_GENERATED_BYTECODE is set.
*
* Performs various checks on the generated bytecode. Most notable is a byte code verifier.
* Individual tests and debug output can be turned on by setting the boolean variables in the
* method body below. Note that these tests slow down bytecode generation considerably.
*
* Note: path names below are Windows only and you'll have to adjust according to your own machine.
* This is for debug purposes only.
*
* In this case, the generated bytecode is produced by javac compiling the java source code generated
* by the JavaSourceGenerator. Thus, the bytecode is bound to be "correct"! The purpose here is mainly
* to allow comparison between the bytecode generated by javac and the bytecode generated by our
* direct-to-bytecode generators such as the ASM generator.
*
*/
private void debugGeneratedBytecode() {
ProgramResourceLocator[] members = resourceRepository.getMembers(moduleFolder);
for (int fileN = 0, nFiles = members.length; fileN < nFiles; ++fileN) {
ProgramResourceLocator member = members[fileN];
String name = member.getName();
// Skip anything which isn't a class file.
if (!(member instanceof ProgramResourceLocator.File) || !name.toLowerCase().endsWith(".class")) {
continue;
}
ProgramResourceLocator.File classFileLocator = (ProgramResourceLocator.File)member;
byte[] bytecode;
InputStream classFileContents = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
classFileContents = resourceRepository.getContents(classFileLocator);
byte[] buf = new byte[4096];
while (true) {
int bytesRead = classFileContents.read(buf);
if (bytesRead < 0) {
break;
}
baos.write(buf, 0, bytesRead);
}
bytecode = baos.toByteArray();
} catch (IOException ioe) {
throw new RuntimeException("Could not read the class file " + member.getName() + ".");
} finally {
if (classFileContents != null) {
try {
classFileContents.close();
} catch (IOException e) {
}
}
}
final boolean dumpAsmifiedText = true;
final boolean dumpDisassembledText = true;
final boolean verifyClassFileFormat = true;
//javac generates many debug op codes, such as line number annotations. For comparison with the asm
//generated byte codes we can skip these.
final boolean skipDebugOpCodes = true;
final boolean skipInnerClassAttributes = false;
String classFileName = member.getName();
String className = classFileName.substring(0, classFileName.length() - ".class".length());
String moduleName = moduleFolder.getModuleName().toString().replaceAll("_", "__").replace('.', '_');
if (dumpAsmifiedText) {
String asmifierDumpPath = "d:\\dev\\asmifierOutput\\javaSource\\" + moduleName + "\\" + className + ".txt";
BytecodeDebuggingUtilities.dumpAsmifiedText(asmifierDumpPath, bytecode, skipDebugOpCodes, skipInnerClassAttributes);
}
if (dumpDisassembledText) {
String disassembyDumpPath = "d:\\dev\\disassembly\\javaSource\\" + moduleName + "\\" + className + ".txt";
BytecodeDebuggingUtilities.dumpDisassembledText(disassembyDumpPath, bytecode, skipDebugOpCodes, skipInnerClassAttributes);
}
if (verifyClassFileFormat) {
BytecodeDebuggingUtilities.verifyClassFileFormat(moduleName + "\\" + className + ".class", bytecode);
}
}
}
/**
* A character stream that collects its output in a limited size string buffer, which can
* then be used to construct a string.
* This class will silently ignore requests to write after the maximum size is reached.
* <p>
* Closing a <tt>FixedSizeStringWriter</tt> has no effect. The methods in this class
* can be called after the stream has been closed without generating an
* <tt>IOException</tt>.
*/
private static class FixedSizeStringWriter extends StringWriter {
private final int maxSize;
/**
* Create a new string writer, using the default initial
* string-buffer size.
*
* @param maxSize
*/
FixedSizeStringWriter(int maxSize) {
super();
this.maxSize = maxSize;
}
/**
* Create a new string writer, using the specified initial string-buffer
* size.
*
* @param initialSize
* an int specifying the initial size of the buffer.
* @param maxSize
*/
FixedSizeStringWriter(int initialSize, int maxSize) {
super(initialSize);
this.maxSize = maxSize;
}
private boolean canWrite () {
return getBuffer().length() < maxSize;
}
/**
* Write a single character.
*
* @param c
*/
@Override
public void write(int c) {
if (canWrite()) {
super.write(c);
}
}
/**
* Write a portion of an array of characters.
*
* @param cbuf
* Array of characters
* @param off
* Offset from which to start writing characters
* @param len
* Number of characters to write
*/
@Override
public void write(char cbuf[], int off, int len) {
if (canWrite()) {
super.write(cbuf, off, len);
}
}
/**
* Write a string.
* @param str
*/
@Override
public void write(String str) {
if (canWrite()) {
super.write(str);
}
}
/**
* Write a portion of a string.
*
* @param str
* String to be written
* @param off
* Offset from which to start writing characters
* @param len
* Number of characters to write
*/
@Override
public void write(String str, int off, int len) {
if (canWrite()) {
super.write(str, off, len);
}
}
}
}