/* * 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); } } } }