package org.openslx.dozmod;
import java.awt.AWTEvent;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.ContainerEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.zip.Deflater;
import javax.net.ssl.SSLContext;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy;
import org.apache.logging.log4j.core.appender.rolling.OnStartupTriggeringPolicy;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.openslx.dozmod.Config.ProxyMode;
import org.openslx.dozmod.gui.Gui;
import org.openslx.dozmod.gui.MainWindow;
import org.openslx.dozmod.gui.helper.I18n;
import org.openslx.dozmod.gui.helper.Language;
import org.openslx.dozmod.gui.helper.MessageType;
import org.openslx.dozmod.util.ClientVersion;
import org.openslx.dozmod.util.ProxyConfigurator;
import org.openslx.thrifthelper.ThriftManager;
import org.openslx.util.AppUtil;
import org.openslx.util.Util;
import com.formdev.flatlaf.FlatDarkLaf;
import com.formdev.flatlaf.FlatLightLaf;
public class App {
// Logger
private final static Logger LOGGER = LogManager.getLogger(App.class);
public static final int THRIFT_PORT = 9090;
public static final int THRIFT_SSL_PORT = THRIFT_PORT + 1;
public static final int THRIFT_TIMEOUT_MS = 15000;
private static CountDownLatch proxyLatch = new CountDownLatch(1);
private static boolean proxyInitDone = false;
private static String masterServerHost = null;
private static String setupFileLogger() {
// path to the log file
final String logFileName = Config.getPath() + File.separator + Branding.getConfigDirectory() + ".log";
final LoggerContext loggingContext = LoggerContext.class.cast(LogManager.getContext(false));
final Configuration loggingConfig = loggingContext.getConfiguration();
// add rolling file appender
final RollingFileAppender fileAppender = RollingFileAppender.newBuilder()
.setName("logToFile")
.withFileName(logFileName)
.withFilePattern(logFileName + ".%i")
.withAppend(true)
.withBufferedIo(true)
.setConfiguration(loggingConfig)
.withCreateOnDemand(false)
.setLayout(PatternLayout.newBuilder()
.withConfiguration(loggingConfig)
.withPattern("[%t] %-5p %F - %m%n")
.build())
.withStrategy(DefaultRolloverStrategy.newBuilder()
.withMin("1")
.withMax("9")
.withFileIndex("1")
.withCompressionLevelStr(Integer.toString(Deflater.NO_COMPRESSION))
.withConfig(loggingConfig)
.build())
.withPolicy(OnStartupTriggeringPolicy.createPolicy(1))
.build();
fileAppender.start();
// register rolling file appender
loggingConfig.addAppender(fileAppender);
loggingConfig.getRootLogger().addAppender(fileAppender, Level.ALL, null);
loggingContext.updateLoggers(loggingConfig);
return logFileName;
}
public static void main(final String[] args) throws InvocationTargetException, InterruptedException
{
// setup basic logging appender to log output on console if no external appender (log4j2.properties) is configured
if (org.apache.logging.log4j.core.Logger.class.cast(LogManager.getRootLogger()).getAppenders().isEmpty()) {
Configurator.initialize(new DefaultConfiguration());
}
if (args.length >= 2) {
if (args[0].equals("--json")) {
writeJsonUpdateFile(args[1]);
return;
}
if (args[0].equals("--dump")) {
Branding.dump(args[1]);
return;
}
}
if (args.length >= 3) {
if (args[0].equals("--pack")) {
Branding.pack(args[1], args[2]);
return;
}
}
try {
Config.init();
} catch (Exception e) {
Gui.showMessageBox(null, I18n.APP.getString("App.Message.error.loadingConfigurationFailed"),
MessageType.ERROR, LOGGER, e);
return;
}
final String logFilePath = setupFileLogger();
AppUtil.logHeader(LOGGER, Branding.getApplicationName(), App.class.getPackage().getImplementationVersion());
LOGGER.info("Starting logging to " + logFilePath);
// Setting the locale
if (!setPreferredLanguage()) {
// Detect operating system language
String ul = System.getProperty("user.language");
if (ul.equals("de")) {
Locale.setDefault(new Locale("de", "DE"));
Config.setPreferredLanguage(Language.DE_DE.value);
} else if (ul.equals("tr")) {
Locale.setDefault(new Locale("tr", "TR"));
Config.setPreferredLanguage(Language.TR_TR.value);
} else {
Locale.setDefault(new Locale("en", "US"));
Config.setPreferredLanguage(Language.EN_US.value);
}
}
// Install FlatLaf look and feel
FlatLightLaf.installLafInfo();
FlatDarkLaf.installLafInfo();
try {
if (System.getProperty("swing.defaultlaf") != null) {
UIManager.setLookAndFeel(System.getProperty("swing.defaultlaf"));
} else if(Config.getLookAndFeel() != null) {
UIManager.setLookAndFeel(Config.getLookAndFeel());
} else {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel");
Config.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel");
}
} catch (Throwable e1) {
try {
LOGGER.error("Something went wrong with the chosen 'LookAndFeel'. Falling back to default 'SystemLookAndFeel'");
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
if(Config.getLookAndFeel() == null) {
Config.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
} catch (Throwable e) {
LOGGER.error("Cannot select system look and feel", e);
LOGGER.error("----------------------------------");
LOGGER.error("Cannot initialize GUI. Make sure you have the non-headless version of Java installed.");
System.exit(1);
}
}
// Setup swing style
System.setProperty("awt.useSystemAAFontSettings", "on");
System.setProperty("swing.aatext", "true");
// Adjust font size
adjustFontSize(Config.getFontScaling());
// Set up connection to master server
final String host;
int port;
boolean useSsl;
if (args.length == 3) {
host = args[0];
port = Util.parseInt(args[1], -1);
useSsl = Boolean.parseBoolean(args[2]);
} else {
host = Branding.getMasterServerAddress();
port = THRIFT_SSL_PORT;
useSsl = true;
}
// remember masterserver host
masterServerHost = host;
// now start the proxy detection
if (Config.getProxyMode() == ProxyMode.AUTO) {
// Initialize the proxy settings
new Thread() {
@Override
public void run() {
ProxyConfigurator.init();
proxyInitDone = true;
proxyLatch.countDown();
}
}.start();
} else {
proxyInitDone = true;
proxyLatch.countDown();
}
// SSL
if (useSsl) {
try {
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
ctx.init(null, null, null);
ThriftManager.setMasterServerAddress(ctx, host, port, THRIFT_TIMEOUT_MS);
} catch (final Exception e1) {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
boolean ret = Gui.showMessageBox(null, I18n.APP.getString("App.Message.yesNo.SSLNotAvailable"),
MessageType.QUESTION_YESNO, LOGGER, e1);
if (!ret) {
System.exit(1);
}
}
});
useSsl = false;
port = port - 1; // This assumes SSL port is always plain port + 1
}
}
// No "else", might be fallback for failed SSL
if (!useSsl) {
ThriftManager.setMasterServerAddress(null, host, port, THRIFT_TIMEOUT_MS);
}
// Setup global thrift connection error handler and then open the GUI
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
if (e instanceof ClassCastException) {
// HACK HACK: Endless chains of exceptions from nowhere on Linux after suspend
// (seems to be driver/version/model specific)
if (e.getMessage().contains("SurfaceData"))
return;
}
Gui.showMessageBox(null,
I18n.APP.getString("App.Message.warning.uncaughtException", t.getName()),
MessageType.WARNING, LOGGER, e);
}
});
MainWindow.open();
}
});
}
private static void writeJsonUpdateFile(String destination) {
try {
ClientVersion.createJson(destination);
} catch (IOException e) {
LOGGER.error("Failed to write JSON update file to '" + destination + "': ", e);
}
}
private static void adjustFontSize(int percent) {
if (percent == 100 || percent <= 0 || percent > 1000)
return;
final float scaling = 0.01f * (float) percent;
int size = determineDefaultFontSize(UIManager.getLookAndFeelDefaults());
if (size == -1) {
size = determineDefaultFontSize(UIManager.getDefaults());
}
if (size == -1) {
size = 12;
}
final float defaultSize = size;
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent event) {
if (event instanceof ContainerEvent) {
ContainerEvent containerEvent = (ContainerEvent) event;
if (containerEvent.getID() == ContainerEvent.COMPONENT_ADDED) {
Font font = containerEvent.getChild().getFont();
// Do not update font in tables and ComboBoxes on every renderer call to prevent weirdness.
// This prevents multiple instances of fonts being scaled multiple times.
if (containerEvent.getChild().getName() != null &&
(containerEvent.getChild().getName().toLowerCase().contains("render") ||
(containerEvent.getChild().getName().toLowerCase().contains("combo")))){
return;
}
if (font != null && font.getSize2D() <= defaultSize) {
containerEvent.getChild().setFont(
new Font(font.getName(), font.getStyle(), Math.round(font.getSize2D()
* scaling)));
}
}
}
}
}, AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.CONTAINER_EVENT_MASK);
Font tbFont = UIManager.getFont("TitledBorder.font");
if (tbFont != null) {
UIManager.put("TitledBorder.font", tbFont.deriveFont(tbFont.getSize2D() * scaling));
}
}
private static int determineDefaultFontSize(UIDefaults defaults) {
if (defaults == null)
return -1;
int sizes[] = new int[100];
Set<Object> keys = new HashSet<>(defaults.keySet());
for (Object key : keys) {
if (key == null)
continue;
Object value = defaults.get(key);
if (value == null)
continue;
if (value instanceof Font) {
Font font = (Font) value;
if (font.getSize() > 0 && font.getSize() < sizes.length) {
sizes[font.getSize()]++;
}
}
}
int best = -1;
for (int index = 0; index < sizes.length; ++index) {
if (best == -1 || sizes[best] < sizes[index]) {
best = index;
}
}
return sizes[best];
}
/**
* Blocks as long as initialization is still going on. Currently this is
* just the proxy setup, so this should be used before any network
* communication happens.
*/
public static void waitForInit() {
if (proxyInitDone)
return;
try {
proxyLatch.await();
} catch (InterruptedException e) {
}
}
/**
* Check if any default language is already set and valid.
* @return true, if any default language exists and is valid.
*/
private static boolean setPreferredLanguage() {
// Check if any preferred language exists
String language = Config.getPreferredLanguage();
// Check whether the provided string is null or empty
if (language == null || language.trim().isEmpty()) {
return false;
}
// Check if the provided string has the format language_country e.g. en_US
String[] parts = language.split("_");
if (parts.length != 2) {
return false;
}
Locale locale = new Locale(parts[0], parts[1]);
// Check now if the locale is valid
if (!Arrays.asList(Locale.getAvailableLocales()).contains(locale)) {
return false;
}
Locale.setDefault(locale);
return true;
}
public static synchronized boolean isInitDone() {
return proxyInitDone;
}
public static synchronized String getMasterServerAddress() {
return masterServerHost;
}
}