/* * Copyright 2012 Red Hat, Inc. and/or its affiliates. * * Licensed under the Eclipse Public License version 1.0, available at * http://www.eclipse.org/legal/epl-v10.html */ package org.jboss.forge.roaster.model.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Properties; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.ToolFactory; import org.eclipse.jdt.core.formatter.CodeFormatter; import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.text.edits.TextEdit; import org.jboss.forge.roaster.model.source.JavaClassSource; import org.jboss.forge.roaster.spi.Streams; /** * Formats Java source code. * * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a> */ public abstract class Formatter { /** * Format the given Java source {@link File}, using the built in code format style. * * @throws IOException When the file cannot be read or written */ public static void format(File source) throws IOException { format(null, source); } /** * Format the given Java source {@link File} using the given Eclipse code format properties {@link File}. * * @throws IOException When the file cannot be read or written, or the preferences cannot be read. */ public static void format(File prefs, File source) throws IOException { Properties options = readConfig(prefs); if (options == null) options = readConfigInternal(); InputStream in = null; OutputStream out = null; try { in = new BufferedInputStream(new FileInputStream(source)); String content = Streams.toString(in); String formatted = format(options, content); out = new BufferedOutputStream(new FileOutputStream(source)); Streams.write(new ByteArrayInputStream(formatted.getBytes()), out); } finally { Streams.closeQuietly(in); Streams.closeQuietly(out); } } /** * Format the given {@link JavaClassSource}, using the built in code format style. */ public static String format(JavaClassSource javaClass) { return format(javaClass.toString()); } /** * Format the given {@link JavaClassSource}, using the given Eclipse code format {@link Properties}. */ public static String format(Properties prefs, JavaClassSource javaClass) { return format(prefs, javaClass.toString()); } /** * Format the given {@link String} as a Java source file, using the built in code format style. */ public static String format(String source) { Properties options = readConfigInternal(); return format(options, source); } /** * Format the given {@link String} as a Java source type, using the given Eclipse code format {@link Properties}. */ public static String format(Properties prefs, String source) { CodeFormatter codeFormatter = ToolFactory.createCodeFormatter(prefs); IDocument doc = new Document(source); try { TextEdit edit = codeFormatter.format(CodeFormatter.K_COMPILATION_UNIT | CodeFormatter.F_INCLUDE_COMMENTS, source, 0, source.length(), 0, null); if (edit != null) { edit.apply(doc); } else { return source; } } catch (BadLocationException e) { throw new RuntimeException(e); } return ensureCorrectNewLines(doc.get()); } private static Properties readConfig(File prefs) throws IOException { if (prefs != null) { InputStream stream = new BufferedInputStream(new FileInputStream(prefs)); try { Properties config = parseConfig(stream); return applyShadedPackageName(config); } catch (IOException e) { throw new IOException("Error reading preferences file: [" + prefs.getAbsolutePath() + "]", e); } finally { Streams.closeQuietly(stream); } } return null; } public static Properties applyShadedPackageName(final Properties config) { Properties modified = new Properties(); String shadePackage = JavaCore.class.getPackage().getName().replaceAll("org\\.eclipse.*$", ""); for (String property : config.stringPropertyNames()) { if (property.startsWith(shadePackage)) { modified.put(property, config.getProperty(property)); } else { modified.put(shadePackage + property, config.getProperty(property)); } } return modified; } /** * The given options should at least provide the source level (JavaCore.COMPILER_SOURCE), the compiler compliance * level (JavaCore.COMPILER_COMPLIANCE) and the target platform (JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM). * * Without these options, it is not possible for the code formatter to know what kind of source it needs to format. */ private static Properties readConfigInternal() { Properties properties = new Properties(); properties.setProperty(JavaCore.COMPILER_SOURCE, CompilerOptions.VERSION_1_8); properties.setProperty(JavaCore.COMPILER_COMPLIANCE, CompilerOptions.VERSION_1_8); properties.setProperty(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, CompilerOptions.VERSION_1_8); // ROASTER-96: Add a blank line after imports. "1" is equivalent to TRUE in the formatter XML file properties.setProperty(DefaultCodeFormatterConstants.FORMATTER_BLANK_LINES_AFTER_IMPORTS, "1"); return properties; } private static Properties parseConfig(InputStream stream) throws IOException { try { final Properties formatterOptions = new Properties(); formatterOptions.load(stream); return formatterOptions; } finally { Streams.closeQuietly(stream); } } private static String ensureCorrectNewLines(String content) { String newLine = System.getProperty("line.separator"); if (content.contains("\n") && !content.contains(newLine)) return content.replaceAll("\n", newLine); return content; } }