Run.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2024, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.UIManager;
import net.sourceforge.plantuml.cli.CliAction;
import net.sourceforge.plantuml.cli.CliFlag;
import net.sourceforge.plantuml.cli.CliOptions;
import net.sourceforge.plantuml.cli.CliParser;
import net.sourceforge.plantuml.cli.ErrorStatus;
import net.sourceforge.plantuml.cli.Exit;
import net.sourceforge.plantuml.cli.GlobalConfig;
import net.sourceforge.plantuml.cli.GlobalConfigKey;
import net.sourceforge.plantuml.code.NoPlantumlCompressionException;
import net.sourceforge.plantuml.code.Transcoder;
import net.sourceforge.plantuml.code.TranscoderUtil;
import net.sourceforge.plantuml.dot.GraphvizRuntimeEnvironment;
import net.sourceforge.plantuml.file.FileGroup;
import net.sourceforge.plantuml.file.SuggestedFile;
import net.sourceforge.plantuml.ftp.FtpServer;
import net.sourceforge.plantuml.klimt.drawing.svg.SvgGraphics;
import net.sourceforge.plantuml.klimt.sprite.SpriteGrayLevel;
import net.sourceforge.plantuml.klimt.sprite.SpriteUtils;
import net.sourceforge.plantuml.log.Logme;
import net.sourceforge.plantuml.picoweb.PicoWebServer;
import net.sourceforge.plantuml.png.MetadataTag;
import net.sourceforge.plantuml.security.SFile;
import net.sourceforge.plantuml.security.SImageIO;
import net.sourceforge.plantuml.security.SecurityUtils;
import net.sourceforge.plantuml.swing.MainWindow;
import net.sourceforge.plantuml.syntax.LanguageDescriptor;
import net.sourceforge.plantuml.utils.Cypher;
import net.sourceforge.plantuml.utils.Log;
import net.sourceforge.plantuml.version.Version;
public class Run {
// ::remove file when __CORE__
// ::remove file when __HAXE__
public static void main(String[] argsArray)
throws NoPlantumlCompressionException, IOException, InterruptedException {
System.setProperty("log4j.debug", "false");
final long start = System.currentTimeMillis();
if (argsArray.length > 0 && argsArray[0].equalsIgnoreCase("-headless"))
System.setProperty("java.awt.headless", "true");
final String display = System.getenv("DISPLAY");
final String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
if (display == null && waylandDisplay != null)
Log.info(() -> "Wayland detected; X11 compatibility may be required via XWayland.");
else if (display != null)
Log.info(() -> "X11 display available: " + display);
else
Log.info(() -> "No display detected; you may need the -headless flag.");
if (GraphicsEnvironment.isHeadless()) {
Log.info(() -> "Forcing -Djava.awt.headless=true");
System.setProperty("java.awt.headless", "true");
}
final String javaAwtHeadless = System.getProperty("java.awt.headless");
if (javaAwtHeadless == null)
Log.info(() -> "java.awt.headless not set");
else
Log.info(() -> "java.awt.headless set as '" + javaAwtHeadless + "'");
final ErrorStatus errorStatus = ErrorStatus.init();
final CliOptions option = CliParser.parse(argsArray);
try {
final String timeout = option.getString(CliFlag.TIMEOUT);
if (timeout != null && timeout.matches("\\d+"))
GlobalConfig.getInstance().put(GlobalConfigKey.TIMEOUT_MS, Integer.parseInt(timeout) * 1000L);
final String charset = option.getString(CliFlag.CHARSET);
Log.info(() -> "Using charset " + charset);
option.flags.triggerNonImmediateCliAction();
final CliAction action = option.flags.getImmediateAction();
if (action != null) {
action.runAction();
return;
}
if (option.isTrue(CliFlag.GRAPHVIZ_DOT)) {
final String v = option.flags.getString(CliFlag.GRAPHVIZ_DOT);
GraphvizRuntimeEnvironment.getInstance()
.setDotExecutable(StringUtils.eventuallyRemoveStartingAndEndingDoubleQuote(v));
}
if (option.flags.isTrue(CliFlag.FTP)) {
goFtp(option);
return;
}
final List<String> remainingArgs = option.flags.getRemainingArgs();
ProgressBar.setEnable(option.isTrue(CliFlag.PROGRESS));
Log.info(() -> "SecurityProfile " + SecurityUtils.getSecurityProfile());
if (GlobalConfig.getInstance().boolValue(GlobalConfigKey.VERBOSE)) {
Log.info(() -> "PlantUML Version " + Version.versionString());
Log.info(() -> "GraphicsEnvironment.isHeadless() " + GraphicsEnvironment.isHeadless());
}
if (option.isTrue(CliFlag.ENCODE_SPRITE)) {
encodeSprite(remainingArgs);
return;
}
if (option.isTrue(CliFlag.PICOWEB) && option.getPicowebPort() != -1) {
goPicoweb(option);
return;
}
forceOpenJdkResourceLoad();
if (GlobalConfig.getInstance().boolValue(GlobalConfigKey.GUI)) {
runGui(option);
return;
}
if (option.isTrue(CliFlag.PIPE) || option.isTrue(CliFlag.PIPEMAP) || option.isTrue(CliFlag.SYNTAX)) {
new Pipe(option, System.out, System.in, charset).managePipe(errorStatus);
if (errorStatus.hasError())
Exit.exit(errorStatus.getExitCode());
return;
}
if (option.isTrue(CliFlag.DECODE_URL)) {
for (String s : option.getRemainingArgs()) {
final Transcoder transcoder = TranscoderUtil.getDefaultTranscoder();
System.out.println("@startuml");
System.out.println(transcoder.decode(s));
System.out.println("@enduml");
}
return;
}
if (option.isTrue(CliFlag.RETRIEVE_METADATA)) {
final List<File> files = new ArrayList<>();
for (String s : option.getRemainingArgs()) {
final FileGroup group = new FileGroup(s, option.getExcludes());
incTotal(group.getFiles().size());
files.addAll(group.getFiles());
}
for (File f : files)
extractMetadata(f);
return;
}
if (option.isTrue(CliFlag.SPLASH))
Splash.createSplash();
final Run runner = new Run(option, errorStatus, charset);
incTotal(runner.size());
if (option.isTrue(CliFlag.COMPUTE_URL))
runner.computeUrl();
else if (option.isTrue(CliFlag.FAIL_FAST2) && runner.checkError())
errorStatus.incError();
else
runner.processInputsInParallel();
} finally {
if (option.isTrue(CliFlag.DURATION)) {
final double duration = (System.currentTimeMillis() - start) / 1000.0;
Log.error("Duration = " + duration + " seconds");
}
}
if (errorStatus.hasError() || errorStatus.isEmpty())
option.getStdrpt().finalMessage(errorStatus);
if (errorStatus.hasError())
Exit.exit(errorStatus.getExitCode());
}
private final CliOptions option;
private final ErrorStatus errorStatus;
private final String charset;
private final List<File> files = new ArrayList<>();
private Cypher cypher;
public Run(CliOptions option, ErrorStatus errorStatus, String charset) {
this.option = option;
this.errorStatus = errorStatus;
this.charset = charset;
for (String s : option.getRemainingArgs()) {
final FileGroup group = new FileGroup(s, option.getExcludes());
for (final File f : group.getFiles())
files.add(f);
}
if (option.getPreprocessorOutputMode() == OptionPreprocOutputMode.CYPHER)
this.cypher = new LanguageDescriptor().getCypher();
Log.info(() -> "Found " + size() + " files");
}
public int size() {
return files.size();
}
@FunctionalInterface
static interface FileTask {
public void processFile(File f) throws IOException, InterruptedException;
}
private boolean checkError() throws InterruptedException {
final AtomicBoolean hasError = new AtomicBoolean();
processInParallel(file -> {
if (hasError.get())
return;
final ISourceFileReader sourceFileReader = getSourceFileReader(file, option, charset);
if (sourceFileReader.hasError()) {
hasError.set(true);
errorStatus.incError();
}
});
return hasError.get();
}
private void computeUrl() throws IOException {
for (File f : files) {
final ISourceFileReader sourceFileReader = getSourceFileReader(f, option, charset);
for (BlockUml s : sourceFileReader.getBlocks())
System.out.println(s.getEncodedUrl());
}
}
private void processInputsInParallel() throws InterruptedException {
SFile lockFile = null;
try {
if (GlobalConfig.getInstance().boolValue(GlobalConfigKey.WORD)) {
final SFile dir = new SFile(option.getRemainingArgs().get(0));
final SFile javaIsRunningFile = dir.file("javaisrunning.tmp");
javaIsRunningFile.delete();
lockFile = dir.file("javaumllock.tmp");
}
processInParallel(file -> {
if (errorStatus.hasError() && option.isFailfastOrFailfast2())
return;
manageFileInternal(file);
incDone(errorStatus.hasError());
});
} finally {
if (lockFile != null)
lockFile.delete();
}
}
private void processInParallel(FileTask task) throws InterruptedException {
Log.info(() -> "Using several threads: " + option.getNbThreads());
final ExecutorService executor = Executors.newFixedThreadPool(option.getNbThreads());
for (File f : files)
executor.submit(() -> {
try {
task.processFile(f);
} catch (IOException | InterruptedException e) {
Logme.error(e);
}
});
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
private void manageFileInternal(File f) throws IOException, InterruptedException {
Log.info(() -> "Working on " + f.getPath());
final ISourceFileReader sourceFileReader = getSourceFileReader(f, option, charset);
if (sourceFileReader.hasError())
errorStatus.incError();
else
errorStatus.incOk();
if (option.isTrue(CliFlag.CHECK_ONLY))
return;
if (option.getPreprocessorOutputMode() != null) {
extractPreproc(sourceFileReader);
return;
}
final List<GeneratedImage> result = sourceFileReader.getGeneratedImages();
final Stdrpt rpt = option.getStdrpt();
if (result.size() == 0) {
Log.error("Warning: no image in " + f.getPath());
rpt.printInfo(System.err, null);
return;
}
for (BlockUml s : sourceFileReader.getBlocks())
rpt.printInfo(System.err, s.getDiagram());
if (result.size() != 0) {
for (GeneratedImage image : result) {
final int lineError = image.lineErrorRaw();
if (lineError != -1) {
rpt.errorLine(lineError, f);
errorStatus.incError();
return;
}
}
errorStatus.incOk();
}
}
private static void runGui(final CliOptions option) {
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (Exception e) {
}
final List<String> list = option.getRemainingArgs();
File dir = null;
if (list.size() == 1) {
final File f = new File(list.get(0));
if (f.exists() && f.isDirectory())
dir = f;
}
try {
new MainWindow(option, dir);
} catch (java.awt.HeadlessException e) {
System.err.println("There is an issue with your server. You will find some tips here:");
System.err.println("https://forum.plantuml.net/3399/problem-with-x11-and-headless-exception");
System.err.println("https://plantuml.com/en/faq#239d64f675c3e515");
throw e;
}
}
public static void forceOpenJdkResourceLoad() {
if (isOpenJdkRunning()) {
// see https://github.com/plantuml/plantuml/issues/123
Log.info(() -> "Forcing resource load on OpenJdk");
final BufferedImage imDummy = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
final Graphics2D gg = imDummy.createGraphics();
final String text = "Alice";
final Font font = new Font("SansSerif", Font.PLAIN, 12);
final FontMetrics fm = gg.getFontMetrics(font);
final Rectangle2D rect = fm.getStringBounds(text, gg);
}
}
public static boolean isOpenJdkRunning() {
final String jvmName = System.getProperty("java.vm.name");
if (jvmName != null && jvmName.toLowerCase().contains("openjdk"))
return true;
return false;
}
static private final String httpProtocol = "http://";
static private final String httpsProtocol = "https://";
private static void encodeSprite(List<String> remainingArgs) throws IOException {
SpriteGrayLevel level = SpriteGrayLevel.GRAY_16;
boolean compressed = false;
final String path;
if (remainingArgs.size() > 1 && remainingArgs.get(0).matches("(4|8|16)z?")) {
if (remainingArgs.get(0).startsWith("8"))
level = SpriteGrayLevel.GRAY_8;
if (remainingArgs.get(0).startsWith("4"))
level = SpriteGrayLevel.GRAY_4;
compressed = StringUtils.goLowerCase(remainingArgs.get(0)).endsWith("z");
path = remainingArgs.get(1);
} else {
path = remainingArgs.get(0);
}
final String fileName;
final URL source;
final String lowerPath = StringUtils.goLowerCase(path);
if (lowerPath.startsWith(httpProtocol) || lowerPath.startsWith(httpsProtocol)) {
source = new java.net.URL(path);
final String p = source.getPath();
fileName = p.substring(p.lastIndexOf('/') + 1, p.length());
} else {
final SFile f = new SFile(path);
source = f.toURI().toURL();
fileName = f.getName();
}
if (source == null)
return;
final BufferedImage im;
try (InputStream stream = source.openStream()) {
im = SImageIO.read(stream);
}
final String name = getSpriteName(fileName);
final String s = compressed ? SpriteUtils.encodeCompressed(im, name, level)
: SpriteUtils.encode(im, name, level);
System.out.println(s);
}
private static String getSpriteName(String fileName) {
final String s = getSpriteNameInternal(fileName);
if (s.length() == 0)
return "test";
return s;
}
private static String getSpriteNameInternal(String fileName) {
final StringBuilder sb = new StringBuilder();
for (char c : fileName.toCharArray())
if (("" + c).matches("[\\p{L}0-9_]"))
sb.append(c);
else
return sb.toString();
return sb.toString();
}
private static void goFtp(CliOptions option) throws IOException {
final int ftpPort = option.getFtpPort();
System.err.println("ftpPort=" + ftpPort);
final FtpServer ftpServer = new FtpServer(ftpPort, option.getFileFormatOption().getFileFormat());
ftpServer.go();
}
private static void goPicoweb(CliOptions option) throws IOException {
PicoWebServer.startServer(option.getPicowebPort(), option.getPicowebBindAddress(),
option.getPicowebEnableStop());
}
public static void printFonts() {
final Font fonts[] = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
for (Font f : fonts)
System.out.println(
"f=" + f + "/" + f.getPSName() + "/" + f.getName() + "/" + f.getFontName() + "/" + f.getFamily());
final String name[] = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
for (String n : name)
System.out.println("n=" + n);
}
private static void incDone(boolean error) {
Splash.incDone(error);
ProgressBar.incDone(error);
}
private static void incTotal(int nb) {
Splash.incTotal(nb);
ProgressBar.incTotal(nb);
}
private static ISourceFileReader getSourceFileReader(File f, CliOptions option, String charset) throws IOException {
final ISourceFileReader sourceFileReader;
final FileFormatOption fileFormatOption = option.getFileFormatOption();
File outputDir = option.getOutputDir();
if (outputDir != null && outputDir.getPath().endsWith("$")) {
final String path = outputDir.getPath();
outputDir = new File(path.substring(0, path.length() - 1)).getAbsoluteFile();
sourceFileReader = new SourceFileReaderCopyCat(option.getDefaultDefines(f), f, outputDir,
option.getConfig(), charset, fileFormatOption);
} else {
sourceFileReader = new SourceFileReader(option.getDefaultDefines(f), f, outputDir, option.getConfig(),
charset, fileFormatOption);
}
sourceFileReader.setCheckMetadata(option.isTrue(CliFlag.CHECK_METADATA));
((SourceFileReaderAbstract) sourceFileReader).setNoerror(option.isTrue(CliFlag.NO_ERROR));
return sourceFileReader;
}
private void extractPreproc(final ISourceFileReader sourceFileReader) throws IOException {
for (BlockUml blockUml : sourceFileReader.getBlocks()) {
final SuggestedFile suggested = ((SourceFileReaderAbstract) sourceFileReader).getSuggestedFile(blockUml)
.withPreprocFormat();
final SFile file = suggested.getFile(0);
Log.info(() -> "Export preprocessing source to " + file.getPrintablePath());
try (final PrintWriter pw = charset == null ? file.createPrintWriter() : file.createPrintWriter(charset)) {
int level = 0;
for (CharSequence cs : blockUml.getDefinition(true)) {
String s = cs.toString();
if (cypher != null) {
if (s.contains("skinparam") && s.contains("{"))
level++;
if (level == 0 && s.contains("skinparam") == false)
s = cypher.cypher(s);
if (level > 0 && s.contains("}"))
level--;
}
pw.println(s);
}
}
}
}
private static void extractMetadata(File f) throws IOException {
System.out.println("------------------------");
System.out.println(f);
System.out.println();
if (f.getName().endsWith(".svg")) {
final SFile file = SFile.fromFile(f);
final String svg = FileUtils.readFile(file);
final int idx = svg.lastIndexOf(SvgGraphics.META_HEADER);
if (idx > 0) {
String part = svg.substring(idx + SvgGraphics.META_HEADER.length());
final int idxEnd = part.indexOf("]");
if (idxEnd > 0) {
part = part.substring(0, idxEnd);
part = part.replace("- -", "--");
final String decoded = TranscoderUtil.getDefaultTranscoderProtected().decode(part);
System.out.println(decoded);
}
}
} else {
final String data = new MetadataTag(f, "plantuml").getData();
System.out.println(data);
}
System.out.println("------------------------");
}
}