cleaning structure
This commit is contained in:
204
old code/tray/src/com/apple/OSXAdapter.java
Executable file
204
old code/tray/src/com/apple/OSXAdapter.java
Executable file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
|
||||
File: OSXAdapter.java
|
||||
|
||||
Abstract: Hooks existing preferences/about/quit functionality from an
|
||||
existing Java app into handlers for the Mac OS X application menu.
|
||||
Uses a Proxy object to dynamically implement the
|
||||
com.apple.eawt.ApplicationListener interface and register it with the
|
||||
com.apple.eawt.Application object. This allows the complete project
|
||||
to be both built and run on any platform without any stubs or
|
||||
placeholders. Useful for developers looking to implement Mac OS X
|
||||
features while supporting multiple platforms with minimal impact.
|
||||
|
||||
Version: 2.0
|
||||
|
||||
Disclaimer: IMPORTANT: This Apple software is supplied to you by
|
||||
Apple Inc. ("Apple") in consideration of your agreement to the
|
||||
following terms, and your use, installation, modification or
|
||||
redistribution of this Apple software constitutes acceptance of these
|
||||
terms. If you do not agree with these terms, please do not use,
|
||||
install, modify or redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a personal, non-exclusive
|
||||
license, under Apple's copyrights in this original Apple software (the
|
||||
"Apple Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc.
|
||||
may be used to endorse or promote products derived from the Apple
|
||||
Software without specific prior written permission from Apple. Except
|
||||
as expressly stated in this notice, no other rights or licenses, express
|
||||
or implied, are granted by Apple herein, including but not limited to
|
||||
any patent rights that may be infringed by your derivative works or by
|
||||
other works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
|
||||
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright (C) 2003-2007 Apple, Inc., All Rights Reserved
|
||||
|
||||
*/
|
||||
|
||||
package com.apple;
|
||||
import java.lang.reflect.*;
|
||||
|
||||
|
||||
public class OSXAdapter implements InvocationHandler {
|
||||
|
||||
protected Object targetObject;
|
||||
protected Method targetMethod;
|
||||
protected String proxySignature;
|
||||
|
||||
static Object macOSXApplication;
|
||||
|
||||
// Pass this method an Object and Method equipped to perform application shutdown logic
|
||||
// The method passed should return a boolean stating whether or not the quit should occur
|
||||
public static void setQuitHandler(Object target, Method quitHandler) {
|
||||
setHandler(new OSXAdapter("handleQuit", target, quitHandler));
|
||||
}
|
||||
|
||||
// Pass this method an Object and Method equipped to display application info
|
||||
// They will be called when the About menu item is selected from the application menu
|
||||
public static void setAboutHandler(Object target, Method aboutHandler) {
|
||||
boolean enableAboutMenu = (target != null && aboutHandler != null);
|
||||
if (enableAboutMenu) {
|
||||
setHandler(new OSXAdapter("handleAbout", target, aboutHandler));
|
||||
}
|
||||
// If we're setting a handler, enable the About menu item by calling
|
||||
// com.apple.eawt.Application reflectively
|
||||
try {
|
||||
Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[] { boolean.class });
|
||||
enableAboutMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enableAboutMenu) });
|
||||
} catch (Exception ex) {
|
||||
System.err.println("OSXAdapter could not access the About Menu");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Pass this method an Object and a Method equipped to display application options
|
||||
// They will be called when the Preferences menu item is selected from the application menu
|
||||
public static void setPreferencesHandler(Object target, Method prefsHandler) {
|
||||
boolean enablePrefsMenu = (target != null && prefsHandler != null);
|
||||
if (enablePrefsMenu) {
|
||||
setHandler(new OSXAdapter("handlePreferences", target, prefsHandler));
|
||||
}
|
||||
// If we're setting a handler, enable the Preferences menu item by calling
|
||||
// com.apple.eawt.Application reflectively
|
||||
try {
|
||||
Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[] { boolean.class });
|
||||
enablePrefsMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enablePrefsMenu) });
|
||||
} catch (Exception ex) {
|
||||
System.err.println("OSXAdapter could not access the About Menu");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Pass this method an Object and a Method equipped to handle document events from the Finder
|
||||
// Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the
|
||||
// application bundle's Info.plist
|
||||
public static void setFileHandler(Object target, Method fileHandler) {
|
||||
setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) {
|
||||
// Override OSXAdapter.callTarget to send information on the
|
||||
// file to be opened
|
||||
public boolean callTarget(Object appleEvent) {
|
||||
if (appleEvent != null) {
|
||||
try {
|
||||
Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[])null);
|
||||
String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[])null);
|
||||
this.targetMethod.invoke(this.targetObject, new Object[] { filename });
|
||||
} catch (Exception ex) {
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener
|
||||
public static void setHandler(OSXAdapter adapter) {
|
||||
try {
|
||||
Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
|
||||
if (macOSXApplication == null) {
|
||||
macOSXApplication = applicationClass.getConstructor((Class[])null).newInstance((Object[])null);
|
||||
}
|
||||
Class<?> applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener");
|
||||
Method addListenerMethod = applicationClass.getDeclaredMethod("addApplicationListener", new Class[] { applicationListenerClass });
|
||||
// Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener
|
||||
Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[] { applicationListenerClass }, adapter);
|
||||
addListenerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
|
||||
} catch (ClassNotFoundException cnfe) {
|
||||
System.err.println("This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled (" + cnfe + ")");
|
||||
} catch (Exception ex) { // Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
|
||||
System.err.println("Mac OS X Adapter could not talk to EAWT:");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example),
|
||||
// the Object that will ultimately perform the task, and the Method to be called on that Object
|
||||
protected OSXAdapter(String proxySignature, Object target, Method handler) {
|
||||
this.proxySignature = proxySignature;
|
||||
this.targetObject = target;
|
||||
this.targetMethod = handler;
|
||||
}
|
||||
|
||||
// Override this method to perform any operations on the event
|
||||
// that comes with the various callbacks
|
||||
// See setFileHandler above for an example
|
||||
public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
|
||||
Object result = targetMethod.invoke(targetObject, (Object[])null);
|
||||
if (result == null) {
|
||||
return true;
|
||||
}
|
||||
return Boolean.valueOf(result.toString()).booleanValue();
|
||||
}
|
||||
|
||||
// InvocationHandler implementation
|
||||
// This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked
|
||||
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
|
||||
if (isCorrectMethod(method, args)) {
|
||||
boolean handled = callTarget(args[0]);
|
||||
setApplicationEventHandled(args[0], handled);
|
||||
}
|
||||
// All of the ApplicationListener methods are void; return null regardless of what happens
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compare the method that was called to the intended method when the OSXAdapter instance was created
|
||||
// (e.g. handleAbout, handleQuit, handleOpenFile, etc.)
|
||||
protected boolean isCorrectMethod(Method method, Object[] args) {
|
||||
return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1);
|
||||
}
|
||||
|
||||
// It is important to mark the ApplicationEvent as handled and cancel the default behavior
|
||||
// This method checks for a boolean result from the proxy method and sets the event accordingly
|
||||
protected void setApplicationEventHandled(Object event, boolean handled) {
|
||||
if (event != null) {
|
||||
try {
|
||||
Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[] { boolean.class });
|
||||
// If the target method returns a boolean, use that as a hint
|
||||
setHandledMethod.invoke(event, new Object[] { Boolean.valueOf(handled) });
|
||||
} catch (Exception ex) {
|
||||
System.err.println("OSXAdapter was unable to handle an ApplicationEvent: " + event);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
old code/tray/src/com/apple/OSXAdapterWrapper.java
Executable file
77
old code/tray/src/com/apple/OSXAdapterWrapper.java
Executable file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package com.apple;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.MacUtilities;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
/**
|
||||
* Java 9+ compatible shim around Apple's OSXAdapter
|
||||
*
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class OSXAdapterWrapper implements InvocationHandler {
|
||||
private static final Logger log = LogManager.getLogger(OSXAdapterWrapper.class);
|
||||
public static final boolean legacyMode = Constants.JAVA_VERSION.lessThan(Version.valueOf("9.0.0"));
|
||||
|
||||
private Object target;
|
||||
private Method handler;
|
||||
|
||||
public OSXAdapterWrapper(Object target, Method handler) {
|
||||
this.target = target;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public static void setQuitHandler(Object target, Method handler) {
|
||||
if (!legacyMode) {
|
||||
// Java 9+
|
||||
wrap("java.awt.desktop.QuitHandler", "setQuitHandler", target, handler);
|
||||
} else {
|
||||
// Java 7, 8
|
||||
OSXAdapter.setAboutHandler(target, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAboutHandler(Object target, Method handler) {
|
||||
if (!legacyMode) {
|
||||
// Java 9+
|
||||
wrap("java.awt.desktop.AboutHandler", "setAboutHandler", target, handler);
|
||||
} else {
|
||||
// Java 7, 8
|
||||
OSXAdapter.setAboutHandler(target, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public static void wrap(String className, String methodName, Object target, Method handler) {
|
||||
try {
|
||||
Class desktop = Desktop.getDesktop().getClass();
|
||||
Class<?> handlerClass = Class.forName(className);
|
||||
Method method = desktop.getDeclaredMethod(methodName, new Class<?>[] {handlerClass});
|
||||
Object proxy = Proxy.newProxyInstance(MacUtilities.class.getClassLoader(), new Class<?>[] {handlerClass}, new OSXAdapterWrapper(target, handler));
|
||||
method.invoke(Desktop.getDesktop(), new Object[] {proxy});
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to set {}", className, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
|
||||
handler.invoke(target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
1
old code/tray/src/javax.usb.properties
Executable file
1
old code/tray/src/javax.usb.properties
Executable file
@@ -0,0 +1 @@
|
||||
javax.usb.services=org.usb4java.javax.Services
|
||||
17
old code/tray/src/log4j2.xml
Executable file
17
old code/tray/src/log4j2.xml
Executable file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration>
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%p] %d{ISO8601} @ %c:%L%n\t%m%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<!-- Packages -->
|
||||
<Logger name="qz" level="trace"/>
|
||||
<Logger name="qz.build.provision" level="info"/>
|
||||
<!-- Default -->
|
||||
<Root level="warn">
|
||||
<AppenderRef ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
96
old code/tray/src/org/dyorgio/jna/platform/mac/ActionCallback.java
Executable file
96
old code/tray/src/org/dyorgio/jna/platform/mac/ActionCallback.java
Executable file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.Callback;
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
@SuppressWarnings("Convert2Lambda")
|
||||
public final class ActionCallback extends NSObject {
|
||||
|
||||
private static final NativeLong actionCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, ActionCallback.class.getSimpleName(), 0);
|
||||
private static final Pointer actionCallbackSel = Foundation.INSTANCE.sel_registerName("actionCallback");
|
||||
private static final Pointer setTargetSel = Foundation.INSTANCE.sel_registerName("setTarget:");
|
||||
private static final Pointer setActionSel = Foundation.INSTANCE.sel_registerName("setAction:");
|
||||
private static final Callback registerActionCallback;
|
||||
|
||||
static {
|
||||
startNativeAppMainThread();
|
||||
registerActionCallback = new Callback() {
|
||||
@SuppressWarnings("unused")
|
||||
public void callback(Pointer self, Pointer selector) {
|
||||
if (selector.equals(actionCallbackSel)) {
|
||||
ActionCallback action;
|
||||
|
||||
synchronized (callbackMap) {
|
||||
action = callbackMap.get(Pointer.nativeValue(self));
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
action.runnable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!Foundation.INSTANCE.class_addMethod(actionCallbackClass,
|
||||
actionCallbackSel, registerActionCallback, "v@:")) {
|
||||
throw new RuntimeException("Error initializing ActionCallback as a objective C class");
|
||||
}
|
||||
|
||||
Foundation.INSTANCE.objc_registerClassPair(actionCallbackClass);
|
||||
}
|
||||
|
||||
private static final HashMap<Long, ActionCallback> callbackMap = new HashMap<Long, ActionCallback>();
|
||||
|
||||
private final Runnable runnable;
|
||||
|
||||
@SuppressWarnings("LeakingThisInConstructor")
|
||||
public ActionCallback(Runnable callable) {
|
||||
super(Foundation.INSTANCE.class_createInstance(actionCallbackClass, 0));
|
||||
this.runnable = callable;
|
||||
synchronized (callbackMap) {
|
||||
callbackMap.put(getId().longValue(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
synchronized (callbackMap) {
|
||||
callbackMap.remove(getId().longValue());
|
||||
}
|
||||
super.release();
|
||||
}
|
||||
|
||||
public void installActionOnNSControl(NativeLong nsControl) {
|
||||
Foundation.INSTANCE.objc_msgSend(nsControl, setTargetSel, id);
|
||||
Foundation.INSTANCE.objc_msgSend(nsControl, setActionSel, actionCallbackSel);
|
||||
}
|
||||
}
|
||||
71
old code/tray/src/org/dyorgio/jna/platform/mac/Foundation.java
Executable file
71
old code/tray/src/org/dyorgio/jna/platform/mac/Foundation.java
Executable file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.Callback;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public interface Foundation extends Library {
|
||||
|
||||
Foundation INSTANCE = Native.load("Foundation", Foundation.class);
|
||||
|
||||
NativeLong class_getInstanceVariable(NativeLong classPointer, String name);
|
||||
|
||||
NativeLong object_getIvar(NativeLong target, NativeLong ivar);
|
||||
|
||||
NativeLong objc_getClass(String className);
|
||||
|
||||
NativeLong objc_allocateClassPair(NativeLong superClass, String name, long extraBytes);
|
||||
|
||||
void objc_registerClassPair(NativeLong clazz);
|
||||
|
||||
NativeLong class_createInstance(NativeLong clazz, int extraBytes);
|
||||
|
||||
boolean class_addMethod(NativeLong clazz, Pointer selector, Callback callback, String types);
|
||||
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector);
|
||||
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Pointer obj);
|
||||
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, NativeLong objAddress);
|
||||
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, boolean boolArg);
|
||||
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, double doubleArg);
|
||||
|
||||
// Used by NSObject.performSelectorOnMainThread
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Pointer selectorDst, NativeLong objAddress, boolean wait);
|
||||
|
||||
// Used by NSString.fromJavaString
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, byte[] bytes, int len, long encoding);
|
||||
|
||||
Pointer sel_registerName(String selectorName);
|
||||
}
|
||||
104
old code/tray/src/org/dyorgio/jna/platform/mac/FoundationUtil.java
Executable file
104
old code/tray/src/org/dyorgio/jna/platform/mac/FoundationUtil.java
Executable file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public final class FoundationUtil {
|
||||
|
||||
private static final Foundation FOUNDATION = Foundation.INSTANCE;
|
||||
|
||||
public static final NativeLong NULL = new NativeLong(0l);
|
||||
|
||||
private FoundationUtil() {
|
||||
}
|
||||
|
||||
public static boolean isNull(NativeLong id) {
|
||||
return NULL.equals(id);
|
||||
}
|
||||
|
||||
public static boolean isNull(NSObject object) {
|
||||
return NULL.equals(object.id);
|
||||
}
|
||||
|
||||
public static boolean isFalse(NativeLong id) {
|
||||
return NULL.equals(id);
|
||||
}
|
||||
|
||||
public static boolean isTrue(NativeLong id) {
|
||||
return !NULL.equals(id);
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, String selector) {
|
||||
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector));
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, String selector, boolean boolArg) {
|
||||
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), boolArg);
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, String selector, double doubleArg) {
|
||||
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), doubleArg);
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, String selector, NativeLong objAddress) {
|
||||
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), objAddress);
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, Pointer selectorPointer) {
|
||||
return FOUNDATION.objc_msgSend(id, selectorPointer);
|
||||
}
|
||||
|
||||
public static NativeLong invoke(NativeLong id, Pointer selectorPointer, NativeLong objAddress) {
|
||||
return FOUNDATION.objc_msgSend(id, selectorPointer, objAddress);
|
||||
}
|
||||
|
||||
public static void runOnMainThreadAndWait(Runnable runnable) throws InterruptedException, ExecutionException {
|
||||
runOnMainThread(runnable, true);
|
||||
}
|
||||
|
||||
public static FutureTask runOnMainThread(Runnable runnable, boolean waitUntilDone) {
|
||||
FutureTask futureTask = new FutureTask(runnable, null);
|
||||
FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone);
|
||||
return futureTask;
|
||||
}
|
||||
|
||||
public static <T> T callOnMainThreadAndWait(Callable<T> callable) throws InterruptedException, ExecutionException {
|
||||
return callOnMainThread(callable, true).get();
|
||||
}
|
||||
|
||||
public static <T> FutureTask<T> callOnMainThread(Callable<T> callable, boolean waitUntilDone) {
|
||||
FutureTask<T> futureTask = new FutureTask(callable);
|
||||
FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone);
|
||||
return futureTask;
|
||||
}
|
||||
}
|
||||
94
old code/tray/src/org/dyorgio/jna/platform/mac/FutureTaskCallback.java
Executable file
94
old code/tray/src/org/dyorgio/jna/platform/mac/FutureTaskCallback.java
Executable file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.Callback;
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
@SuppressWarnings("Convert2Lambda")
|
||||
class FutureTaskCallback<T> extends NSObject {
|
||||
|
||||
private static final NativeLong futureTaskCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, FutureTaskCallback.class.getSimpleName(), 0);
|
||||
private static final Pointer futureTaskCallbackSel = Foundation.INSTANCE.sel_registerName("futureTaskCallback");
|
||||
private static final Callback registerFutureTaskCallback;
|
||||
|
||||
static {
|
||||
startNativeAppMainThread();
|
||||
registerFutureTaskCallback = new Callback() {
|
||||
@SuppressWarnings("unused")
|
||||
public void callback(Pointer self, Pointer selector) {
|
||||
if (selector.equals(futureTaskCallbackSel)) {
|
||||
FutureTaskCallback action;
|
||||
|
||||
synchronized (callbackMap) {
|
||||
action = callbackMap.remove(Pointer.nativeValue(self));
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
action.callable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!Foundation.INSTANCE.class_addMethod(futureTaskCallbackClass,
|
||||
futureTaskCallbackSel, registerFutureTaskCallback, "v@:")) {
|
||||
throw new RuntimeException("Error initializing FutureTaskCallback as a objective C class");
|
||||
}
|
||||
|
||||
Foundation.INSTANCE.objc_registerClassPair(futureTaskCallbackClass);
|
||||
}
|
||||
|
||||
private static final HashMap<Long, FutureTaskCallback> callbackMap = new HashMap<Long, FutureTaskCallback>();
|
||||
|
||||
private final FutureTask<T> callable;
|
||||
|
||||
@SuppressWarnings("LeakingThisInConstructor")
|
||||
private FutureTaskCallback(FutureTask<T> callable) {
|
||||
super(Foundation.INSTANCE.class_createInstance(futureTaskCallbackClass, 0));
|
||||
this.callable = callable;
|
||||
synchronized (callbackMap) {
|
||||
callbackMap.put(getId().longValue(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
synchronized (callbackMap) {
|
||||
callbackMap.remove(getId().longValue());
|
||||
}
|
||||
super.release();
|
||||
}
|
||||
|
||||
static <T> void performOnMainThread(FutureTask<T> futureTask, boolean waitUntilDone) {
|
||||
new FutureTaskCallback(futureTask).performSelectorOnMainThread(futureTaskCallbackSel, null, waitUntilDone);
|
||||
}
|
||||
}
|
||||
22
old code/tray/src/org/dyorgio/jna/platform/mac/NSApplication.java
Executable file
22
old code/tray/src/org/dyorgio/jna/platform/mac/NSApplication.java
Executable file
@@ -0,0 +1,22 @@
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
public class NSApplication extends NSObject {
|
||||
static NativeLong klass = Objc.INSTANCE.objc_lookUpClass("NSApplication");
|
||||
static Pointer sharedApplication = Objc.INSTANCE.sel_getUid("sharedApplication");
|
||||
static Pointer activateIgnoringOtherApps = Objc.INSTANCE.sel_getUid("activateIgnoringOtherApps:");
|
||||
|
||||
public static NSApplication sharedApplication() {
|
||||
return new NSApplication(Objc.INSTANCE.objc_msgSend(klass, sharedApplication));
|
||||
}
|
||||
|
||||
public NSApplication(NativeLong handle) {
|
||||
super(handle);
|
||||
}
|
||||
|
||||
public void activateIgnoringOtherApps(boolean flag) {
|
||||
Objc.INSTANCE.objc_msgSend(this.getId(), activateIgnoringOtherApps, flag);
|
||||
}
|
||||
}
|
||||
50
old code/tray/src/org/dyorgio/jna/platform/mac/NSDictionary.java
Executable file
50
old code/tray/src/org/dyorgio/jna/platform/mac/NSDictionary.java
Executable file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public class NSDictionary extends NSObject {
|
||||
|
||||
private static final NativeLong dictionaryClass = Foundation.INSTANCE.objc_getClass("NSDictionary");
|
||||
private static final Pointer dictionaryWithContentsOfFileSel = Foundation.INSTANCE.sel_registerName("dictionaryWithContentsOfFile:");
|
||||
private static final Pointer objectForKeySel = Foundation.INSTANCE.sel_registerName("objectForKey:");
|
||||
|
||||
public NSDictionary(NativeLong id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public static NSDictionary dictionaryWithContentsOfFile(NSString file) {
|
||||
return new NSDictionary(Foundation.INSTANCE.objc_msgSend(dictionaryClass, dictionaryWithContentsOfFileSel, file.id));
|
||||
}
|
||||
|
||||
public NSObject objectForKey(NSObject key) {
|
||||
return new NSString(FoundationUtil.invoke(id, objectForKeySel, key.id));
|
||||
}
|
||||
}
|
||||
96
old code/tray/src/org/dyorgio/jna/platform/mac/NSObject.java
Executable file
96
old code/tray/src/org/dyorgio/jna/platform/mac/NSObject.java
Executable file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
import java.awt.*;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public class NSObject {
|
||||
|
||||
static final NativeLong objectClass = Foundation.INSTANCE.objc_getClass("NSObject");
|
||||
protected static final Pointer allocSel = Foundation.INSTANCE.sel_registerName("alloc");
|
||||
protected static final Pointer initSel = Foundation.INSTANCE.sel_registerName("init");
|
||||
protected static final Pointer releaseSel = Foundation.INSTANCE.sel_registerName("release");
|
||||
protected static final Pointer performSelectorOnMainThread$withObject$waitUntilDoneSel
|
||||
= Foundation.INSTANCE.sel_registerName("performSelectorOnMainThread:withObject:waitUntilDone:");
|
||||
|
||||
final NativeLong id;
|
||||
|
||||
public NSObject(NativeLong id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public final NativeLong getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
Foundation.INSTANCE.objc_msgSend(id, releaseSel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"FinalizeDeclaration","deprecation","removal"})
|
||||
protected void finalize() throws Throwable {
|
||||
release();
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
public void performSelectorOnMainThread(Pointer selector, NativeLong object, boolean waitUntilDone) {
|
||||
Foundation.INSTANCE.objc_msgSend(id, //
|
||||
NSObject.performSelectorOnMainThread$withObject$waitUntilDoneSel, //
|
||||
selector, object, waitUntilDone);
|
||||
}
|
||||
|
||||
static volatile boolean initialized = false;
|
||||
|
||||
static void startNativeAppMainThread() {
|
||||
if (!initialized) {
|
||||
synchronized (NSObject.objectClass) {
|
||||
if (!initialized) {
|
||||
try {
|
||||
if(EventQueue.isDispatchThread()) {
|
||||
Toolkit.getDefaultToolkit();
|
||||
} else {
|
||||
SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit());
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (InvocationTargetException ex) {
|
||||
// ignore
|
||||
} finally {
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
old code/tray/src/org/dyorgio/jna/platform/mac/NSString.java
Executable file
74
old code/tray/src/org/dyorgio/jna/platform/mac/NSString.java
Executable file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.platform.mac.CoreFoundation;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public class NSString extends NSObject {
|
||||
|
||||
public static final Charset UTF_16LE_CHARSET = Charset.forName("UTF-16LE");
|
||||
|
||||
private static final NativeLong stringCls = Foundation.INSTANCE.objc_getClass("NSString");
|
||||
private static final Pointer stringSel = Foundation.INSTANCE.sel_registerName("string");
|
||||
private static final Pointer initWithBytesLengthEncodingSel = Foundation.INSTANCE.sel_registerName("initWithBytes:length:encoding:");
|
||||
private static final long NSUTF16LittleEndianStringEncoding = 0x94000100;
|
||||
|
||||
public NSString(String string) {
|
||||
this(fromJavaString(string));
|
||||
}
|
||||
|
||||
public NSString(NativeLong id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (FoundationUtil.isNull(this)) {
|
||||
return null;
|
||||
}
|
||||
CoreFoundation.CFStringRef cfString = new CoreFoundation.CFStringRef(new Pointer(id.longValue()));
|
||||
try {
|
||||
return CoreFoundation.INSTANCE.CFStringGetLength(cfString).intValue() > 0 ? cfString.stringValue() : "";
|
||||
} finally {
|
||||
cfString.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeLong fromJavaString(String s) {
|
||||
if (s.isEmpty()) {
|
||||
return Foundation.INSTANCE.objc_msgSend(stringCls, stringSel);
|
||||
}
|
||||
|
||||
byte[] utf16Bytes = s.getBytes(UTF_16LE_CHARSET);
|
||||
return Foundation.INSTANCE.objc_msgSend(Foundation.INSTANCE.objc_msgSend(stringCls, allocSel),
|
||||
initWithBytesLengthEncodingSel, utf16Bytes, utf16Bytes.length, NSUTF16LittleEndianStringEncoding);
|
||||
}
|
||||
}
|
||||
50
old code/tray/src/org/dyorgio/jna/platform/mac/NSUserDefaults.java
Executable file
50
old code/tray/src/org/dyorgio/jna/platform/mac/NSUserDefaults.java
Executable file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright 2020 dyorgio.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author dyorgio
|
||||
*/
|
||||
public class NSUserDefaults extends NSObject {
|
||||
|
||||
private static final NativeLong userDefaultsClass = Foundation.INSTANCE.objc_getClass("NSUserDefaults");
|
||||
private static final Pointer standardUserDefaultsdSel = Foundation.INSTANCE.sel_registerName("standardUserDefaults");
|
||||
private static final Pointer stringForKeySel = Foundation.INSTANCE.sel_registerName("stringForKey:");
|
||||
|
||||
public NSUserDefaults(NativeLong id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public static NSUserDefaults standard() {
|
||||
return new NSUserDefaults(Foundation.INSTANCE.objc_msgSend(userDefaultsClass, standardUserDefaultsdSel));
|
||||
}
|
||||
|
||||
public NSString stringForKey(NSString key) {
|
||||
return new NSString(FoundationUtil.invoke(id, stringForKeySel, key.id));
|
||||
}
|
||||
}
|
||||
12
old code/tray/src/org/dyorgio/jna/platform/mac/Objc.java
Executable file
12
old code/tray/src/org/dyorgio/jna/platform/mac/Objc.java
Executable file
@@ -0,0 +1,12 @@
|
||||
package org.dyorgio.jna.platform.mac;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.NativeLong;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
public interface Objc extends Library {
|
||||
Objc INSTANCE = Native.load("objc", Objc.class);
|
||||
NativeLong objc_lookUpClass(String name);
|
||||
Pointer sel_getUid(String str);
|
||||
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Object... args);
|
||||
}
|
||||
190
old code/tray/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java
Executable file
190
old code/tray/src/org/jdesktop/swinghelper/tray/JXTrayIcon.java
Executable file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle,
|
||||
* Santa Clara, California 95054, U.S.A. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
package org.jdesktop.swinghelper.tray;
|
||||
|
||||
import qz.common.Constants;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.PopupMenuListener;
|
||||
import javax.swing.event.PopupMenuEvent;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
public class JXTrayIcon extends TrayIcon {
|
||||
private JPopupMenu menu;
|
||||
private static JDialog dialog;
|
||||
static {
|
||||
dialog = new JDialog((Frame) null);
|
||||
dialog.setUndecorated(true);
|
||||
dialog.setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
private static PopupMenuListener popupListener = new PopupMenuListener() {
|
||||
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
|
||||
}
|
||||
|
||||
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
|
||||
dialog.setVisible(false);
|
||||
}
|
||||
|
||||
public void popupMenuCanceled(PopupMenuEvent e) {
|
||||
dialog.setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public JXTrayIcon(Image image) {
|
||||
super(image);
|
||||
addMouseListener(new MouseAdapter() {
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
showJPopupMenu(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void showJPopupMenu(MouseEvent mouseEvent) {
|
||||
if (menu != null) {
|
||||
Point location = mouseEvent.getLocationOnScreen();
|
||||
// Handle HiDPI factor discrepancy between mouse position and window position
|
||||
if(SystemUtilities.isWindows() && Constants.JAVA_VERSION.getMajorVersion() >= 9) {
|
||||
location.setLocation(location.getX() / WindowsUtilities.getScaleFactor(), location.getY() / WindowsUtilities.getScaleFactor());
|
||||
}
|
||||
showJPopupMenu((int)location.getX(), (int)location.getY());
|
||||
}
|
||||
}
|
||||
|
||||
protected void showJPopupMenu(int x, int y) {
|
||||
dialog.setLocation(x, y);
|
||||
dialog.setVisible(true);
|
||||
|
||||
// Show the menu centered on the invisible dialog
|
||||
menu.show(dialog.getContentPane(), 0, 0);
|
||||
|
||||
// Compensate position variance due to off-screen placement
|
||||
Dimension menuSize = menu.getPreferredSize();
|
||||
Point menuLocation = menu.getLocationOnScreen();
|
||||
int x2 = x; int y2 = y;
|
||||
if (menuLocation.getY() > y) y2 = y + menuSize.height;
|
||||
if (menuLocation.getY() < y) y2 = y - menuSize.height;
|
||||
if (menuLocation.getX() > x) x2 = x + menuSize.width;
|
||||
if (menuLocation.getX() < x) x2 = x - menuSize.width;
|
||||
if(x2 != x || y2 != y) {
|
||||
menu.setLocation(x2, y2);
|
||||
}
|
||||
|
||||
// popup works only for focused windows
|
||||
dialog.toFront();
|
||||
}
|
||||
|
||||
public JPopupMenu getJPopupMenu() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
public void setJPopupMenu(JPopupMenu menu) {
|
||||
if (this.menu != null) {
|
||||
this.menu.removePopupMenuListener(popupListener);
|
||||
}
|
||||
this.menu = menu;
|
||||
menu.addPopupMenuListener(popupListener);
|
||||
}
|
||||
|
||||
private static void createGui() {
|
||||
JXTrayIcon tray = new JXTrayIcon(createImage());
|
||||
tray.setJPopupMenu(createJPopupMenu());
|
||||
try {
|
||||
SystemTray.getSystemTray().add(tray);
|
||||
} catch (AWTException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
|
||||
SwingUtilities.invokeLater(() -> createGui());
|
||||
}
|
||||
|
||||
static Image createImage() {
|
||||
BufferedImage i = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = (Graphics2D) i.getGraphics();
|
||||
g2.setColor(Color.RED);
|
||||
g2.fill(new Ellipse2D.Float(0, 0, i.getWidth(), i.getHeight()));
|
||||
g2.dispose();
|
||||
return i;
|
||||
}
|
||||
|
||||
static JPopupMenu createJPopupMenu() {
|
||||
final JPopupMenu m = new JPopupMenu();
|
||||
m.add(new JMenuItem("Item 1"));
|
||||
m.add(new JMenuItem("Item 2"));
|
||||
JMenu submenu = new JMenu("Submenu");
|
||||
submenu.add(new JMenuItem("item 1"));
|
||||
submenu.add(new JMenuItem("item 2"));
|
||||
submenu.add(new JMenuItem("item 3"));
|
||||
m.add(submenu);
|
||||
JMenuItem exitItem = new JMenuItem("Exit");
|
||||
exitItem.addActionListener(new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
m.add(exitItem);
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getSize() {
|
||||
Dimension iconSize = new Dimension(super.getSize());
|
||||
switch(SystemUtilities.getOs()) {
|
||||
// macOS icons are slightly smaller than the size reported
|
||||
case MAC:
|
||||
// Handle retina display
|
||||
int macScale = MacUtilities.getScaleFactor();
|
||||
|
||||
// Handle undocumented icon border (e.g. 20px has 16px icon)
|
||||
// See also IconCache.fixTrayIcons()
|
||||
iconSize.width -= iconSize.width / 5;
|
||||
iconSize.height -= iconSize.height / 5;
|
||||
|
||||
return new Dimension(iconSize.width * macScale, iconSize.height * macScale);
|
||||
case WINDOWS:
|
||||
if(Constants.JAVA_VERSION.getMajorVersion() >= 9) {
|
||||
// JDK9+ required for HiDPI tray icons on Windows
|
||||
double winScale = WindowsUtilities.getScaleFactor();
|
||||
|
||||
// Handle undocumented HiDPI icon support
|
||||
// Requires TrayIcon.setImageAutoSize(true);
|
||||
iconSize.width *= winScale;
|
||||
iconSize.height *= winScale;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return iconSize;
|
||||
}
|
||||
}
|
||||
812
old code/tray/src/org/joor/Reflect.java
Executable file
812
old code/tray/src/org/joor/Reflect.java
Executable file
@@ -0,0 +1,812 @@
|
||||
/**
|
||||
* Copyright (c) 2011-2013, Lukas Eder, lukas.eder@gmail.com
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is licensed to you under the Apache License, Version 2.0
|
||||
* (the "License"); You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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 "jOOR" 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.
|
||||
*/
|
||||
package org.joor;
|
||||
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Member;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A wrapper for an {@link Object} or {@link Class} upon which reflective calls
|
||||
* can be made.
|
||||
* <p>
|
||||
* An example of using <code>Reflect</code> is <code><pre>
|
||||
* // Static import all reflection methods to decrease verbosity
|
||||
* import static org.joor.Reflect.*;
|
||||
*
|
||||
* // Wrap an Object / Class / class name with the on() method:
|
||||
* on("java.lang.String")
|
||||
* // Invoke constructors using the create() method:
|
||||
* .create("Hello World")
|
||||
* // Invoke methods using the call() method:
|
||||
* .call("toString")
|
||||
* // Retrieve the wrapped object
|
||||
*
|
||||
* @author Lukas Eder
|
||||
* @author Irek Matysiewicz
|
||||
*/
|
||||
public class Reflect {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Static API used as entrance points to the fluent API
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wrap a class name.
|
||||
* <p>
|
||||
* This is the same as calling <code>on(Class.forName(name))</code>
|
||||
*
|
||||
* @param name A fully qualified class name
|
||||
* @return A wrapped class object, to be used for further reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
* @see #on(Class)
|
||||
*/
|
||||
public static Reflect on(String name) throws ReflectException {
|
||||
return on(forName(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a class name, loading it via a given class loader.
|
||||
* <p>
|
||||
* This is the same as calling
|
||||
* <code>on(Class.forName(name, classLoader))</code>
|
||||
*
|
||||
* @param name A fully qualified class name.
|
||||
* @param classLoader The class loader in whose context the class should be
|
||||
* loaded.
|
||||
* @return A wrapped class object, to be used for further reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
* @see #on(Class)
|
||||
*/
|
||||
public static Reflect on(String name, ClassLoader classLoader) throws ReflectException {
|
||||
return on(forName(name, classLoader));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a class.
|
||||
* <p>
|
||||
* Use this when you want to access static fields and methods on a
|
||||
* {@link Class} object, or as a basis for constructing objects of that
|
||||
* class using {@link #create(Object...)}
|
||||
*
|
||||
* @param clazz The class to be wrapped
|
||||
* @return A wrapped class object, to be used for further reflection.
|
||||
*/
|
||||
public static Reflect on(Class<?> clazz) {
|
||||
return new Reflect(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an object.
|
||||
* <p>
|
||||
* Use this when you want to access instance fields and methods on any
|
||||
* {@link Object}
|
||||
*
|
||||
* @param object The object to be wrapped
|
||||
* @return A wrapped object, to be used for further reflection.
|
||||
*/
|
||||
public static Reflect on(Object object) {
|
||||
return new Reflect(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conveniently render an {@link AccessibleObject} accessible.
|
||||
* <p>
|
||||
* To prevent {@link SecurityException}, this is only done if the argument
|
||||
* object and its declaring class are non-public.
|
||||
*
|
||||
* @param accessible The object to render accessible
|
||||
* @return The argument object rendered accessible
|
||||
*/
|
||||
public static <T extends AccessibleObject> T accessible(T accessible) {
|
||||
if (accessible == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (accessible instanceof Member) {
|
||||
Member member = (Member) accessible;
|
||||
|
||||
if (Modifier.isPublic(member.getModifiers()) &&
|
||||
Modifier.isPublic(member.getDeclaringClass().getModifiers())) {
|
||||
|
||||
return accessible;
|
||||
}
|
||||
}
|
||||
|
||||
// [jOOQ #3392] The accessible flag is set to false by default, also for public members.
|
||||
if (!accessible.isAccessible()) {
|
||||
accessible.setAccessible(true);
|
||||
}
|
||||
|
||||
return accessible;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Members
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The wrapped object
|
||||
*/
|
||||
private final Object object;
|
||||
|
||||
/**
|
||||
* A flag indicating whether the wrapped object is a {@link Class} (for
|
||||
* accessing static fields and methods), or any other type of {@link Object}
|
||||
* (for accessing instance fields and methods).
|
||||
*/
|
||||
private final boolean isClass;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructors
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private Reflect(Class<?> type) {
|
||||
this.object = type;
|
||||
this.isClass = true;
|
||||
}
|
||||
|
||||
private Reflect(Object object) {
|
||||
this.object = object;
|
||||
this.isClass = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Fluent Reflection API
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the wrapped object
|
||||
*
|
||||
* @param <T> A convenience generic parameter for automatic unsafe casting
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get() {
|
||||
return (T) object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a field value.
|
||||
* <p>
|
||||
* This is roughly equivalent to {@link Field#set(Object, Object)}. If the
|
||||
* wrapped object is a {@link Class}, then this will set a value to a static
|
||||
* member field. If the wrapped object is any other {@link Object}, then
|
||||
* this will set a value to an instance member field.
|
||||
*
|
||||
* @param name The field name
|
||||
* @param value The new field value
|
||||
* @return The same wrapped object, to be used for further reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
*/
|
||||
public Reflect set(String name, Object value) throws ReflectException {
|
||||
try {
|
||||
Field field = field0(name);
|
||||
field.set(object, unwrap(value));
|
||||
return this;
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a field value.
|
||||
* <p>
|
||||
* This is roughly equivalent to {@link Field#get(Object)}. If the wrapped
|
||||
* object is a {@link Class}, then this will get a value from a static
|
||||
* member field. If the wrapped object is any other {@link Object}, then
|
||||
* this will get a value from an instance member field.
|
||||
* <p>
|
||||
* If you want to "navigate" to a wrapped version of the field, use
|
||||
* {@link #field(String)} instead.
|
||||
*
|
||||
* @param name The field name
|
||||
* @return The field value
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
* @see #field(String)
|
||||
*/
|
||||
public <T> T get(String name) throws ReflectException {
|
||||
return field(name).<T>get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a wrapped field.
|
||||
* <p>
|
||||
* This is roughly equivalent to {@link Field#get(Object)}. If the wrapped
|
||||
* object is a {@link Class}, then this will wrap a static member field. If
|
||||
* the wrapped object is any other {@link Object}, then this wrap an
|
||||
* instance member field.
|
||||
*
|
||||
* @param name The field name
|
||||
* @return The wrapped field
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
*/
|
||||
public Reflect field(String name) throws ReflectException {
|
||||
try {
|
||||
Field field = field0(name);
|
||||
return on(field.get(object));
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Field field0(String name) throws ReflectException {
|
||||
Class<?> type = type();
|
||||
|
||||
// Try getting a public field
|
||||
try {
|
||||
return type.getField(name);
|
||||
}
|
||||
|
||||
// Try again, getting a non-public field
|
||||
catch(NoSuchFieldException e) {
|
||||
do {
|
||||
try {
|
||||
return accessible(type.getDeclaredField(name));
|
||||
}
|
||||
catch(NoSuchFieldException ignore) {}
|
||||
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
while(type != null);
|
||||
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Map containing field names and wrapped values for the fields'
|
||||
* values.
|
||||
* <p>
|
||||
* If the wrapped object is a {@link Class}, then this will return static
|
||||
* fields. If the wrapped object is any other {@link Object}, then this will
|
||||
* return instance fields.
|
||||
* <p>
|
||||
* These two calls are equivalent <code><pre>
|
||||
* on(object).field("myField");
|
||||
* on(object).fields().get("myField");
|
||||
* </pre></code>
|
||||
*
|
||||
* @return A map containing field names and wrapped values.
|
||||
*/
|
||||
public Map<String, Reflect> fields() {
|
||||
Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
|
||||
Class<?> type = type();
|
||||
|
||||
do {
|
||||
for(Field field : type.getDeclaredFields()) {
|
||||
if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
|
||||
String name = field.getName();
|
||||
|
||||
if (!result.containsKey(name)) { result.put(name, field(name)); }
|
||||
}
|
||||
}
|
||||
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
while(type != null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a method by its name.
|
||||
* <p>
|
||||
* This is a convenience method for calling
|
||||
* <code>call(name, new Object[0])</code>
|
||||
*
|
||||
* @param name The method name
|
||||
* @return The wrapped method result or the same wrapped object if the
|
||||
* method returns <code>void</code>, to be used for further
|
||||
* reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
* @see #call(String, Object...)
|
||||
*/
|
||||
public Reflect call(String name) throws ReflectException {
|
||||
return call(name, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a method by its name.
|
||||
* <p>
|
||||
* This is roughly equivalent to {@link Method#invoke(Object, Object...)}.
|
||||
* If the wrapped object is a {@link Class}, then this will invoke a static
|
||||
* method. If the wrapped object is any other {@link Object}, then this will
|
||||
* invoke an instance method.
|
||||
* <p>
|
||||
* Just like {@link Method#invoke(Object, Object...)}, this will try to wrap
|
||||
* primitive types or unwrap primitive type wrappers if applicable. If
|
||||
* several methods are applicable, by that rule, the first one encountered
|
||||
* is called. i.e. when calling <code><pre>
|
||||
* on(...).call("method", 1, 1);
|
||||
* </pre></code> The first of the following methods will be called:
|
||||
* <code><pre>
|
||||
* public void method(int param1, Integer param2);
|
||||
* public void method(Integer param1, int param2);
|
||||
* public void method(Number param1, Number param2);
|
||||
* public void method(Number param1, Object param2);
|
||||
* public void method(int param1, Object param2);
|
||||
* </pre></code>
|
||||
* <p>
|
||||
* The best matching method is searched for with the following strategy:
|
||||
* <ol>
|
||||
* <li>public method with exact signature match in class hierarchy</li>
|
||||
* <li>non-public method with exact signature match on declaring class</li>
|
||||
* <li>public method with similar signature in class hierarchy</li>
|
||||
* <li>non-public method with similar signature on declaring class</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param name The method name
|
||||
* @param args The method arguments
|
||||
* @return The wrapped method result or the same wrapped object if the
|
||||
* method returns <code>void</code>, to be used for further
|
||||
* reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
*/
|
||||
public Reflect call(String name, Object... args) throws ReflectException {
|
||||
Class<?>[] types = types(args);
|
||||
|
||||
// Try invoking the "canonical" method, i.e. the one with exact
|
||||
// matching argument types
|
||||
try {
|
||||
Method method = exactMethod(name, types);
|
||||
return on(method, object, args);
|
||||
}
|
||||
|
||||
// If there is no exact match, try to find a method that has a "similar"
|
||||
// signature if primitive argument types are converted to their wrappers
|
||||
catch (NoSuchMethodException e) {
|
||||
try {
|
||||
Method method = similarMethod(name, types);
|
||||
return on(method, object, args);
|
||||
} catch (NoSuchMethodException e1) {
|
||||
throw new ReflectException(e1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a method with the exact same signature as desired.
|
||||
* <p>
|
||||
* If a public method is found in the class hierarchy, this method is returned.
|
||||
* Otherwise a private method with the exact same signature is returned.
|
||||
* If no exact match could be found, we let the {@code NoSuchMethodException} pass through.
|
||||
*/
|
||||
private Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
|
||||
Class<?> type = type();
|
||||
|
||||
// first priority: find a public method with exact signature match in class hierarchy
|
||||
try {
|
||||
return type.getMethod(name, types);
|
||||
}
|
||||
|
||||
// second priority: find a private method with exact signature match on declaring class
|
||||
catch (NoSuchMethodException e) {
|
||||
do {
|
||||
try {
|
||||
return type.getDeclaredMethod(name, types);
|
||||
}
|
||||
catch(NoSuchMethodException ignore) {}
|
||||
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
while(type != null);
|
||||
|
||||
throw new NoSuchMethodException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a method with a similar signature as desired using
|
||||
* {@link #isSimilarSignature(java.lang.reflect.Method, String, Class[])}.
|
||||
* <p>
|
||||
* First public methods are searched in the class hierarchy, then private
|
||||
* methods on the declaring class. If a method could be found, it is
|
||||
* returned, otherwise a {@code NoSuchMethodException} is thrown.
|
||||
*/
|
||||
private Method similarMethod(String name, Class<?>[] types) throws NoSuchMethodException {
|
||||
Class<?> type = type();
|
||||
|
||||
// first priority: find a public method with a "similar" signature in class hierarchy
|
||||
// similar interpreted in when primitive argument types are converted to their wrappers
|
||||
for (Method method : type.getMethods()) {
|
||||
if (isSimilarSignature(method, name, types)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
// second priority: find a non-public method with a "similar" signature on declaring class
|
||||
do {
|
||||
for(Method method : type.getDeclaredMethods()) {
|
||||
if (isSimilarSignature(method, name, types)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
type = type.getSuperclass();
|
||||
}
|
||||
while(type != null);
|
||||
|
||||
throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types) + " could be found on type " + type() + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a method has a "similar" signature, especially if wrapping
|
||||
* primitive argument types would result in an exactly matching signature.
|
||||
*/
|
||||
private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName, Class<?>[] desiredParamTypes) {
|
||||
return possiblyMatchingMethod.getName().equals(desiredMethodName) && match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a constructor.
|
||||
* <p>
|
||||
* This is a convenience method for calling
|
||||
* <code>create(new Object[0])</code>
|
||||
*
|
||||
* @return The wrapped new object, to be used for further reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
* @see #create(Object...)
|
||||
*/
|
||||
public Reflect create() throws ReflectException {
|
||||
return create(new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a constructor.
|
||||
* <p>
|
||||
* This is roughly equivalent to {@link Constructor#newInstance(Object...)}.
|
||||
* If the wrapped object is a {@link Class}, then this will create a new
|
||||
* object of that class. If the wrapped object is any other {@link Object},
|
||||
* then this will create a new object of the same type.
|
||||
* <p>
|
||||
* Just like {@link Constructor#newInstance(Object...)}, this will try to
|
||||
* wrap primitive types or unwrap primitive type wrappers if applicable. If
|
||||
* several constructors are applicable, by that rule, the first one
|
||||
* encountered is called. i.e. when calling <code><pre>
|
||||
* on(C.class).create(1, 1);
|
||||
* </pre></code> The first of the following constructors will be applied:
|
||||
* <code><pre>
|
||||
* public C(int param1, Integer param2);
|
||||
* public C(Integer param1, int param2);
|
||||
* public C(Number param1, Number param2);
|
||||
* public C(Number param1, Object param2);
|
||||
* public C(int param1, Object param2);
|
||||
* </pre></code>
|
||||
*
|
||||
* @param args The constructor arguments
|
||||
* @return The wrapped new object, to be used for further reflection.
|
||||
* @throws ReflectException If any reflection exception occurred.
|
||||
*/
|
||||
public Reflect create(Object... args) throws ReflectException {
|
||||
Class<?>[] types = types(args);
|
||||
|
||||
// Try invoking the "canonical" constructor, i.e. the one with exact
|
||||
// matching argument types
|
||||
try {
|
||||
Constructor<?> constructor = type().getDeclaredConstructor(types);
|
||||
return on(constructor, args);
|
||||
}
|
||||
|
||||
// If there is no exact match, try to find one that has a "similar"
|
||||
// signature if primitive argument types are converted to their wrappers
|
||||
catch (NoSuchMethodException e) {
|
||||
for(Constructor<?> constructor : type().getDeclaredConstructors()) {
|
||||
if (match(constructor.getParameterTypes(), types)) {
|
||||
return on(constructor, args);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a proxy for the wrapped object allowing to typesafely invoke
|
||||
* methods on it using a custom interface
|
||||
*
|
||||
* @param proxyType The interface type that is implemented by the proxy
|
||||
* @return A proxy for the wrapped object
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <P> P as(Class<P> proxyType) {
|
||||
final boolean isMap = (object instanceof Map);
|
||||
final InvocationHandler handler = new InvocationHandler() {
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
String name = method.getName();
|
||||
|
||||
// Actual method name matches always come first
|
||||
try {
|
||||
return on(object).call(name, args).get();
|
||||
}
|
||||
|
||||
// [#14] Emulate POJO behaviour on wrapped map objects
|
||||
catch (ReflectException e) {
|
||||
if (isMap) {
|
||||
Map<String, Object> map = (Map<String, Object>) object;
|
||||
int length = (args == null ? 0 : args.length);
|
||||
|
||||
if (length == 0 && name.startsWith("get")) {
|
||||
return map.get(property(name.substring(3)));
|
||||
}
|
||||
else if (length == 0 && name.startsWith("is")) {
|
||||
return map.get(property(name.substring(2)));
|
||||
}
|
||||
else if (length == 1 && name.startsWith("set")) {
|
||||
map.put(property(name.substring(3)), args[0]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[] { proxyType }, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the POJO property name of an getter/setter
|
||||
*/
|
||||
private static String property(String string) {
|
||||
int length = string.length();
|
||||
|
||||
if (length == 0) {
|
||||
return "";
|
||||
}
|
||||
else if (length == 1) {
|
||||
return string.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
else {
|
||||
return string.substring(0, 1).toLowerCase(Locale.ENGLISH) + string.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Object API
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check whether two arrays of types match, converting primitive types to
|
||||
* their corresponding wrappers.
|
||||
*/
|
||||
private boolean match(Class<?>[] declaredTypes, Class<?>[] actualTypes) {
|
||||
if (declaredTypes.length == actualTypes.length) {
|
||||
for (int i = 0; i < actualTypes.length; i++) {
|
||||
if (actualTypes[i] == NULL.class)
|
||||
continue;
|
||||
|
||||
if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i])))
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return object.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Reflect) {
|
||||
return object.equals(((Reflect) obj).get());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Utility methods
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wrap an object created from a constructor
|
||||
*/
|
||||
private static Reflect on(Constructor<?> constructor, Object... args) throws ReflectException {
|
||||
try {
|
||||
return on(accessible(constructor).newInstance(args));
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an object returned from a method
|
||||
*/
|
||||
private static Reflect on(Method method, Object object, Object... args) throws ReflectException {
|
||||
try {
|
||||
accessible(method);
|
||||
|
||||
if (method.getReturnType() == void.class) {
|
||||
method.invoke(object, args);
|
||||
return on(object);
|
||||
}
|
||||
else {
|
||||
return on(method.invoke(object, args));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap an object
|
||||
*/
|
||||
private static Object unwrap(Object object) {
|
||||
if (object instanceof Reflect) {
|
||||
return ((Reflect) object).get();
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of types for an array of objects
|
||||
*
|
||||
* @see Object#getClass()
|
||||
*/
|
||||
private static Class<?>[] types(Object... values) {
|
||||
if (values == null) {
|
||||
return new Class[0];
|
||||
}
|
||||
|
||||
Class<?>[] result = new Class[values.length];
|
||||
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
Object value = values[i];
|
||||
result[i] = value == null ? NULL.class : value.getClass();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a class
|
||||
*
|
||||
* @see Class#forName(String)
|
||||
*/
|
||||
private static Class<?> forName(String name) throws ReflectException {
|
||||
try {
|
||||
return Class.forName(name);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> forName(String name, ClassLoader classLoader) throws ReflectException {
|
||||
try {
|
||||
return Class.forName(name, true, classLoader);
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new ReflectException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the wrapped object.
|
||||
*
|
||||
* @see Object#getClass()
|
||||
*/
|
||||
public Class<?> type() {
|
||||
if (isClass) {
|
||||
return (Class<?>) object;
|
||||
}
|
||||
else {
|
||||
return object.getClass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a wrapper type for a primitive type, or the argument type itself, if
|
||||
* it is not a primitive type.
|
||||
*/
|
||||
public static Class<?> wrapper(Class<?> type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
else if (type.isPrimitive()) {
|
||||
if (boolean.class == type) {
|
||||
return Boolean.class;
|
||||
}
|
||||
else if (int.class == type) {
|
||||
return Integer.class;
|
||||
}
|
||||
else if (long.class == type) {
|
||||
return Long.class;
|
||||
}
|
||||
else if (short.class == type) {
|
||||
return Short.class;
|
||||
}
|
||||
else if (byte.class == type) {
|
||||
return Byte.class;
|
||||
}
|
||||
else if (double.class == type) {
|
||||
return Double.class;
|
||||
}
|
||||
else if (float.class == type) {
|
||||
return Float.class;
|
||||
}
|
||||
else if (char.class == type) {
|
||||
return Character.class;
|
||||
}
|
||||
else if (void.class == type) {
|
||||
return Void.class;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private static class NULL {}
|
||||
}
|
||||
79
old code/tray/src/org/joor/ReflectException.java
Executable file
79
old code/tray/src/org/joor/ReflectException.java
Executable file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2011-2013, Lukas Eder, lukas.eder@gmail.com
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is licensed to you under the Apache License, Version 2.0
|
||||
* (the "License"); You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* 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 "jOOR" 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.
|
||||
*/
|
||||
package org.joor;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
/**
|
||||
* A unchecked wrapper for any of Java's checked reflection exceptions:
|
||||
* <p>
|
||||
* These exceptions are
|
||||
* <ul>
|
||||
* <li> {@link ClassNotFoundException}</li>
|
||||
* <li> {@link IllegalAccessException}</li>
|
||||
* <li> {@link IllegalArgumentException}</li>
|
||||
* <li> {@link InstantiationException}</li>
|
||||
* <li> {@link InvocationTargetException}</li>
|
||||
* <li> {@link NoSuchMethodException}</li>
|
||||
* <li> {@link NoSuchFieldException}</li>
|
||||
* <li> {@link SecurityException}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Lukas Eder
|
||||
*/
|
||||
public class ReflectException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Generated UID
|
||||
*/
|
||||
private static final long serialVersionUID = -6213149635297151442L;
|
||||
|
||||
public ReflectException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ReflectException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ReflectException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ReflectException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
133
old code/tray/src/qz/App.java
Executable file
133
old code/tray/src/qz/App.java
Executable file
@@ -0,0 +1,133 @@
|
||||
package qz;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
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.SizeBasedTriggeringPolicy;
|
||||
import org.apache.logging.log4j.core.filter.ThresholdFilter;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.installer.certificate.CertificateManager;
|
||||
import qz.installer.certificate.ExpiryTask;
|
||||
import qz.installer.certificate.KeyPairWrapper;
|
||||
import qz.installer.certificate.NativeCertificateInstaller;
|
||||
import qz.installer.provision.ProvisionInstaller;
|
||||
import qz.ui.PairingConfigDialog;
|
||||
import qz.utils.*;
|
||||
import qz.ws.PrintSocketServer;
|
||||
import qz.ws.SingleInstanceChecker;
|
||||
import qz.ws.substitutions.Substitutions;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Properties;
|
||||
|
||||
public class App {
|
||||
private static final Logger log = LogManager.getLogger(App.class);
|
||||
private static Properties trayProperties = null;
|
||||
|
||||
public static void main(String ... args) {
|
||||
ArgParser parser = new ArgParser(args);
|
||||
LibUtilities.getInstance().bind();
|
||||
if(parser.intercept()) {
|
||||
FileUtilities.cleanup();
|
||||
System.exit(parser.getExitCode());
|
||||
}
|
||||
SingleInstanceChecker.stealWebsocket = parser.hasFlag(ArgValue.STEAL);
|
||||
setupFileLogging();
|
||||
log.info(Constants.ABOUT_TITLE + " version: {}", Constants.VERSION);
|
||||
log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY);
|
||||
log.info("Java version: {}", Constants.JAVA_VERSION.toString());
|
||||
log.info("Java vendor: {}", Constants.JAVA_VENDOR);
|
||||
Substitutions.setEnabled(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_ENABLE));
|
||||
Substitutions.setStrict(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_STRICT));
|
||||
|
||||
CertificateManager certManager = null;
|
||||
try {
|
||||
// Gets and sets the SSL info, properties file
|
||||
certManager = Installer.getInstance().certGen(false);
|
||||
trayProperties = certManager.getProperties();
|
||||
// Reoccurring (e.g. hourly) cert expiration check
|
||||
new ExpiryTask(certManager).schedule();
|
||||
} catch(Exception e) {
|
||||
log.error("Something went critically wrong loading HTTPS", e);
|
||||
}
|
||||
Installer.getInstance().addUserSettings();
|
||||
|
||||
// Load overridable preferences set in qz-tray.properties file
|
||||
NetworkUtilities.setPreferences(certManager.getProperties());
|
||||
SingleInstanceChecker.setPreferences(certManager.getProperties());
|
||||
|
||||
// Linux needs the cert installed in user-space on every launch for Chrome SSL to work
|
||||
if(!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
|
||||
X509Certificate caCert = certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert();
|
||||
// Only install if a CA cert exists (e.g. one we generated)
|
||||
if(caCert != null) {
|
||||
NativeCertificateInstaller.getInstance().install(certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert());
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke any provisioning steps that are phase=startup
|
||||
try {
|
||||
ProvisionInstaller provisionInstaller = new ProvisionInstaller(SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR));
|
||||
provisionInstaller.invoke(Phase.STARTUP);
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred provisioning \"phase\": \"startup\" entries", e);
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
|
||||
// Start the WebSocket
|
||||
PrintSocketServer.runServer(certManager, parser.isHeadless());
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Could not start tray manager", e);
|
||||
}
|
||||
FileUtilities.cleanup();
|
||||
log.warn("The web socket server is no longer running");
|
||||
|
||||
// Show pairing config dialog if needed
|
||||
PairingConfigDialog.showIfNeeded(null); // null for no parent frame, or pass your main frame if available
|
||||
}
|
||||
|
||||
public static Properties getTrayProperties() {
|
||||
return trayProperties;
|
||||
}
|
||||
|
||||
private static void setupFileLogging() {
|
||||
//disable jetty logging
|
||||
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog");
|
||||
System.setProperty("org.eclipse.jetty.LEVEL", "OFF");
|
||||
|
||||
if(PrefsSearch.getBoolean(ArgValue.LOG_DISABLE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int logSize = PrefsSearch.getInt(ArgValue.LOG_SIZE);
|
||||
int logRotate = PrefsSearch.getInt(ArgValue.LOG_ROTATE);
|
||||
Installer.getInstance().cleanupLegacyLogs(Math.max(logRotate, 5));
|
||||
RollingFileAppender fileAppender = RollingFileAppender.newBuilder()
|
||||
.setName("log-file")
|
||||
.withAppend(true)
|
||||
.setLayout(PatternLayout.newBuilder().withPattern("%d{ISO8601} [%p] %m%n").build())
|
||||
.setFilter(ThresholdFilter.createFilter(Level.DEBUG, Filter.Result.ACCEPT, Filter.Result.DENY))
|
||||
.withFileName(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log")
|
||||
.withFilePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".%i.log")
|
||||
.withStrategy(DefaultRolloverStrategy.newBuilder()
|
||||
.withMax(String.valueOf(logRotate))
|
||||
.withFileIndex("min")
|
||||
.build())
|
||||
.withPolicy(SizeBasedTriggeringPolicy.createPolicy(String.valueOf(logSize)))
|
||||
.withImmediateFlush(true)
|
||||
.build();
|
||||
fileAppender.start();
|
||||
|
||||
LoggerUtilities.getRootLogger().addAppender(fileAppender);
|
||||
}
|
||||
}
|
||||
70
old code/tray/src/qz/auth/CRL.java
Executable file
70
old code/tray/src/qz/auth/CRL.java
Executable file
@@ -0,0 +1,70 @@
|
||||
package qz.auth;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import qz.utils.ConnectionUtilities;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Wrapper class for the Certificate Revocation List
|
||||
* Created by Steven on 2/4/2015. Package: qz.auth Project: qz-print
|
||||
*/
|
||||
public class CRL {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(CRL.class);
|
||||
|
||||
/** The URL to the QZ CRL. Should not be changed except for dev tests */
|
||||
public static final String CRL_URL = "https://crl.qz.io";
|
||||
|
||||
private static CRL instance = null;
|
||||
|
||||
private ArrayList<String> revokedHashes = new ArrayList<String>();
|
||||
private boolean loaded = false;
|
||||
|
||||
|
||||
private CRL() {}
|
||||
|
||||
public static CRL getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new CRL();
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("Loading CRL from {}", CRL_URL);
|
||||
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(CRL_URL, false)))) {
|
||||
String line;
|
||||
while((line = br.readLine()) != null) {
|
||||
//Ignore empty and commented lines
|
||||
if (!line.isEmpty() && line.charAt(0) != '#') {
|
||||
instance.revokedHashes.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
instance.loaded = true;
|
||||
log.info("Successfully loaded {} CRL entries from {}", instance.revokedHashes.size(), CRL_URL);
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Unable to access CRL from {}, {}", CRL_URL, e.toString());
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean isRevoked(String fingerprint) {
|
||||
return revokedHashes.contains(fingerprint);
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
536
old code/tray/src/qz/auth/Certificate.java
Executable file
536
old code/tray/src/qz/auth/Certificate.java
Executable file
@@ -0,0 +1,536 @@
|
||||
package qz.auth;
|
||||
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.ssl.Base64;
|
||||
import org.apache.commons.ssl.X509CertificateChainBuilder;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.jce.PrincipalUtil;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.App;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.*;
|
||||
import java.security.cert.*;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Created by Steven on 1/27/2015. Package: qz.auth Project: qz-print
|
||||
* Wrapper to store certificate objects from
|
||||
*/
|
||||
public class Certificate {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(Certificate.class);
|
||||
private static final String QUIETLY_FAIL = "quiet";
|
||||
|
||||
public enum Algorithm {
|
||||
SHA1("SHA1withRSA"),
|
||||
SHA256("SHA256withRSA"),
|
||||
SHA512("SHA512withRSA");
|
||||
|
||||
String name;
|
||||
|
||||
Algorithm(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public static ArrayList<Certificate> rootCAs = new ArrayList<>();
|
||||
public static Certificate builtIn;
|
||||
private static CertPathValidator validator;
|
||||
private static CertificateFactory factory;
|
||||
private static boolean trustBuiltIn = false;
|
||||
// id-at-description used for storing renewal information
|
||||
private static ASN1ObjectIdentifier RENEWAL_OF = new ASN1ObjectIdentifier("2.5.4.13");
|
||||
|
||||
public static final String[] saveFields = new String[] {"fingerprint", "commonName", "organization", "validFrom", "validTo", "valid"};
|
||||
|
||||
public static final String SPONSORED_CN_PREFIX = "Sponsored:";
|
||||
|
||||
// Valid date range allows UI to only show "Expired" text for valid certificates
|
||||
private static final Instant UNKNOWN_MIN = LocalDateTime.MIN.toInstant(ZoneOffset.UTC);
|
||||
private static final Instant UNKNOWN_MAX = LocalDateTime.MAX.toInstant(ZoneOffset.UTC);
|
||||
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
public static final DateTimeFormatter DATE_PARSE = DateTimeFormatter.ofPattern("uuuu-MM-dd['T'][ ]HH:mm:ss[.n]['Z']"); //allow parsing of both ISO and custom formatted dates
|
||||
|
||||
private X509Certificate theCertificate;
|
||||
private boolean sponsored;
|
||||
private String fingerprint;
|
||||
private String commonName;
|
||||
private String organization;
|
||||
private Instant validFrom;
|
||||
private Instant validTo;
|
||||
|
||||
//used by review sites UI only
|
||||
private boolean expired = false;
|
||||
private boolean valid = false;
|
||||
private boolean rootCA = false; // TODO: Move to constructor?
|
||||
|
||||
|
||||
//Pre-set certificate for use when missing
|
||||
public static final Certificate UNKNOWN;
|
||||
|
||||
static {
|
||||
HashMap<String,String> map = new HashMap<>();
|
||||
map.put("fingerprint", "UNKNOWN REQUEST");
|
||||
map.put("commonName", "An anonymous request");
|
||||
map.put("organization", "Unknown");
|
||||
map.put("validFrom", UNKNOWN_MIN.toString());
|
||||
map.put("validTo", UNKNOWN_MAX.toString());
|
||||
map.put("valid", "false");
|
||||
UNKNOWN = Certificate.loadCertificate(map);
|
||||
}
|
||||
|
||||
static {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
validator = CertPathValidator.getInstance("PKIX");
|
||||
factory = CertificateFactory.getInstance("X.509");
|
||||
builtIn = new Certificate("-----BEGIN CERTIFICATE-----\n" +
|
||||
"MIIELzCCAxegAwIBAgIJALm151zCHDxiMA0GCSqGSIb3DQEBCwUAMIGsMQswCQYD\n" +
|
||||
"VQQGEwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UE\n" +
|
||||
"CgwSUVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBM\n" +
|
||||
"TEMxGTAXBgNVBAMMEHF6aW5kdXN0cmllcy5jb20xJzAlBgkqhkiG9w0BCQEWGHN1\n" +
|
||||
"cHBvcnRAcXppbmR1c3RyaWVzLmNvbTAgFw0xNTAzMDEyMzM4MjlaGA8yMTE1MDMw\n" +
|
||||
"MjIzMzgyOVowgawxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTESMBAGA1UEBwwJ\n" +
|
||||
"Q2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJbmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsM\n" +
|
||||
"ElFaIEluZHVzdHJpZXMsIExMQzEZMBcGA1UEAwwQcXppbmR1c3RyaWVzLmNvbTEn\n" +
|
||||
"MCUGCSqGSIb3DQEJARYYc3VwcG9ydEBxemluZHVzdHJpZXMuY29tMIIBIjANBgkq\n" +
|
||||
"hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuWsBa6uk+RM4OKBZTRfIIyqaaFD71FAS\n" +
|
||||
"7kojAQ+ySMpYuqLjIVZuCh92o1FGBvyBKUFc6knAHw5749yhLCYLXhzWwiNW2ri1\n" +
|
||||
"Jwx/d83Wnaw6qA3lt++u3tmiA8tsFtss0QZW0YBpFsIqhamvB3ypwu0bdUV/oH7g\n" +
|
||||
"/s8TFR5LrDfnfxlLFYhTUVWuWzMqEFAGnFG3uw/QMWZnQgkGbx0LMcYzdqFb7/vz\n" +
|
||||
"rTSHfjJsisUTWPjo7SBnAtNYCYaGj0YH5RFUdabnvoTdV2XpA5IPYa9Q597g/M0z\n" +
|
||||
"icAjuaK614nKXDaAUCbjki8RL3OK9KY920zNFboq/jKG6rKW2t51ZQIDAQABo1Aw\n" +
|
||||
"TjAdBgNVHQ4EFgQUA0XGTcD6jqkL2oMPQaVtEgZDqV4wHwYDVR0jBBgwFoAUA0XG\n" +
|
||||
"TcD6jqkL2oMPQaVtEgZDqV4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" +
|
||||
"AQEAijcT5QMVqrWWqpNEe1DidzQfSnKo17ZogHW+BfUbxv65JbDIntnk1XgtLTKB\n" +
|
||||
"VAdIWUtGZbXxrp16NEsh96V2hjDIoiAaEpW+Cp6AHhIVgVh7Q9Knq9xZ1t6H8PL5\n" +
|
||||
"QiYQKQgJ0HapdCxlPKBfUm/Mj1ppNl9mPFJwgHmzORexbxrzU/M5i2jlies+CXNq\n" +
|
||||
"cvmF2l33QNHnLwpFGwYKs08pyHwUPp6+bfci6lRvavztgvnKroWWIRq9ZPlC0yVK\n" +
|
||||
"FFemhbCd7ZVbrTo0NcWZM1PTAbvlOikV9eh3i1Vot+3dJ8F27KwUTtnV0B9Jrxum\n" +
|
||||
"W9P3C48mvwTxYZJFOu0N9UBLLg==\n" +
|
||||
"-----END CERTIFICATE-----");
|
||||
|
||||
builtIn.valid = true;
|
||||
setTrustBuiltIn(true);
|
||||
scanAdditionalCAs();
|
||||
}
|
||||
catch(NoSuchAlgorithmException | CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void scanAdditionalCAs() {
|
||||
ArrayList<Map.Entry<Path, String>> certPaths = new ArrayList<>();
|
||||
// First, look for "authcert.override", "-DtrustedRootCert"
|
||||
certPaths.addAll(FileUtilities.parseDelimitedPaths(PrefsSearch.getString(ArgValue.AUTHCERT_OVERRIDE, App.getTrayProperties())));
|
||||
|
||||
// Second, look for "override.crt" within App directory
|
||||
certPaths.add(new AbstractMap.SimpleEntry<>(SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT), QUIETLY_FAIL));
|
||||
|
||||
for(Map.Entry<Path, String> certPath : certPaths) {
|
||||
if(certPath.getKey() != null) {
|
||||
if (certPath.getKey().toFile().exists()) {
|
||||
try {
|
||||
Certificate caCert = new Certificate(FileUtilities.readLocalFile(certPath.getKey()));
|
||||
caCert.rootCA = true;
|
||||
caCert.valid = true;
|
||||
if(!rootCAs.contains(caCert)) {
|
||||
log.debug("Adding CA certificate: CN={}, O={} ({})",
|
||||
caCert.getCommonName(), caCert.getOrganization(), caCert.getFingerprint());
|
||||
rootCAs.add(caCert);
|
||||
} else {
|
||||
log.warn("CA cert exists, skipping: {}", certPath.getKey());
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Error loading CA cert: {}", certPath.getKey(), e);
|
||||
}
|
||||
} else if(!certPath.getValue().equals(QUIETLY_FAIL)) {
|
||||
log.warn("CA cert \"{}\" was provided, but could not be found, skipping.", certPath.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Certificate(Path path) throws IOException, CertificateException {
|
||||
this(new String(Files.readAllBytes(path), Charsets.UTF_8));
|
||||
}
|
||||
|
||||
/** Decodes a certificate and intermediate certificate from the given string */
|
||||
public Certificate(String in) throws CertificateException {
|
||||
try {
|
||||
//Strip beginning and end
|
||||
String[] split = in.split("--START INTERMEDIATE CERT--");
|
||||
byte[] serverCertificate = Base64.decodeBase64(split[0].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
|
||||
|
||||
X509Certificate theIntermediateCertificate;
|
||||
if (split.length == 2) {
|
||||
byte[] intermediateCertificate = Base64.decodeBase64(split[1].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
|
||||
theIntermediateCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(intermediateCertificate));
|
||||
} else {
|
||||
theIntermediateCertificate = null; //Self-signed
|
||||
}
|
||||
|
||||
//Generate cert
|
||||
theCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(serverCertificate));
|
||||
commonName = getSubjectX509Principal(theCertificate, BCStyle.CN);
|
||||
if(commonName.isEmpty()) {
|
||||
throw new CertificateException("Common Name cannot be blank.");
|
||||
}
|
||||
// Remove "Sponsored: " from CN, we'll swap the trusted icon instead <3
|
||||
if(commonName.startsWith(SPONSORED_CN_PREFIX)) {
|
||||
commonName = commonName.split(SPONSORED_CN_PREFIX)[1].trim();
|
||||
sponsored = true;
|
||||
} else {
|
||||
sponsored = false;
|
||||
}
|
||||
fingerprint = makeThumbPrint(theCertificate);
|
||||
organization = getSubjectX509Principal(theCertificate, BCStyle.O);
|
||||
validFrom = theCertificate.getNotBefore().toInstant();
|
||||
validTo = theCertificate.getNotAfter().toInstant();
|
||||
|
||||
// Check trust anchor against all root certs
|
||||
Certificate foundRoot = null;
|
||||
if(!this.rootCA) {
|
||||
for(Certificate rootCA : rootCAs) {
|
||||
HashSet<X509Certificate> chain = new HashSet<>();
|
||||
try {
|
||||
chain.add(rootCA.theCertificate);
|
||||
if (theIntermediateCertificate != null) { chain.add(theIntermediateCertificate); }
|
||||
X509Certificate[] x509Certificates = X509CertificateChainBuilder.buildPath(theCertificate, chain);
|
||||
|
||||
Set<TrustAnchor> anchor = new HashSet<>();
|
||||
anchor.add(new TrustAnchor(rootCA.theCertificate, null));
|
||||
PKIXParameters params = new PKIXParameters(anchor);
|
||||
params.setRevocationEnabled(false); // TODO: Re-enable, remove proprietary CRL
|
||||
validator.validate(factory.generateCertPath(Arrays.asList(x509Certificates)), params);
|
||||
foundRoot = rootCA;
|
||||
valid = true;
|
||||
log.debug("Successfully chained certificate: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
|
||||
break; // if successful, don't attempt another chain
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.warn("Problem building certificate chain (normal if multiple CAs are in use)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for expiration
|
||||
Instant now = Instant.now();
|
||||
if (expired = (validFrom.isAfter(now) || validTo.isBefore(now))) {
|
||||
log.warn("Certificate is expired: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// If cert matches a rootCA trust it blindly
|
||||
// If cert is chained to a 3rd party rootCA, trust it blindly as well
|
||||
Iterator<Certificate> allCerts = rootCAs.iterator();
|
||||
while(allCerts.hasNext()) {
|
||||
Certificate cert = allCerts.next();
|
||||
if(cert.equals(this) || (cert.equals(foundRoot) && !cert.equals(builtIn))) {
|
||||
log.debug("Adding {} to {} list", cert.toString(), Constants.ALLOW_FILE);
|
||||
if(!isSaved()) {
|
||||
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data());
|
||||
}
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readRenewalInfo();
|
||||
CRL qzCrl = CRL.getInstance();
|
||||
if (qzCrl.isLoaded()) {
|
||||
if (qzCrl.isRevoked(getFingerprint()) || (theIntermediateCertificate != null && qzCrl.isRevoked(makeThumbPrint(theIntermediateCertificate)))) {
|
||||
log.error("Certificate has been revoked and can no longer be used: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
//Assume nothing is revoked, because we can't get the CRL
|
||||
log.warn("Failed to retrieve QZ CRL, skipping CRL check");
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
CertificateException certificateException = new CertificateException();
|
||||
certificateException.initCause(e);
|
||||
throw certificateException;
|
||||
}
|
||||
}
|
||||
|
||||
private void readRenewalInfo() throws Exception {
|
||||
Vector values = PrincipalUtil.getSubjectX509Principal(theCertificate).getValues(RENEWAL_OF);
|
||||
Iterator renewals = values.iterator();
|
||||
|
||||
while(renewals.hasNext()) {
|
||||
String renewalInfo = String.valueOf(renewals.next());
|
||||
|
||||
String renewalPrefix = "renewal-of-";
|
||||
if (!renewalInfo.startsWith(renewalPrefix)) {
|
||||
log.warn("Malformed renewal info: {}", renewalInfo);
|
||||
continue;
|
||||
}
|
||||
String previousFingerprint = renewalInfo.substring(renewalPrefix.length());
|
||||
if (previousFingerprint.length() != 40) {
|
||||
log.warn("Malformed renewal fingerprint: {}", previousFingerprint);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add this certificate to the whitelist if the previous certificate was whitelisted
|
||||
|
||||
// First, handle shared directory
|
||||
File sharedFile = FileUtilities.getFile(Constants.ALLOW_FILE, false);
|
||||
if (existsInAnyFile(previousFingerprint, sharedFile) && !isSaved(false)) {
|
||||
if(!FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), false)) {
|
||||
// Fallback to local directory if shared is not writable
|
||||
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), /* fallback */ true);
|
||||
}
|
||||
}
|
||||
|
||||
// Second, handle local directory
|
||||
File localFile = FileUtilities.getFile(Constants.ALLOW_FILE, true);
|
||||
if (existsInAnyFile(previousFingerprint, localFile) && !isSaved(true)) {
|
||||
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Certificate() {}
|
||||
|
||||
|
||||
/**
|
||||
* Used to rebuild a certificate for the 'Saved Sites' screen without having to decrypt the certificates again
|
||||
*/
|
||||
public static Certificate loadCertificate(HashMap<String,String> data) {
|
||||
Certificate cert = new Certificate();
|
||||
|
||||
cert.fingerprint = data.get("fingerprint");
|
||||
cert.commonName = data.get("commonName");
|
||||
cert.organization = data.get("organization");
|
||||
|
||||
try {
|
||||
cert.validFrom = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validFrom"))).atZone(ZoneOffset.UTC));
|
||||
cert.validTo = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validTo"))).atZone(ZoneOffset.UTC));
|
||||
}
|
||||
catch(DateTimeException e) {
|
||||
cert.validFrom = UNKNOWN_MIN;
|
||||
cert.validTo = UNKNOWN_MAX;
|
||||
|
||||
log.warn("Unable to parse certificate date: {}", e.getMessage());
|
||||
}
|
||||
|
||||
cert.valid = Boolean.parseBoolean(data.get("valid"));
|
||||
|
||||
return cert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks given signature for given data against this certificate,
|
||||
* ensuring it is properly signed
|
||||
*
|
||||
* @param signature the signature appended to the data, base64 encoded
|
||||
* @param data the data to check
|
||||
* @return true if signature valid, false if not
|
||||
*/
|
||||
public boolean isSignatureValid(Algorithm algorithm, String signature, String data) {
|
||||
if (!signature.isEmpty()) {
|
||||
//On errors, assume failure.
|
||||
try {
|
||||
Signature verifier = Signature.getInstance(algorithm.name);
|
||||
verifier.initVerify(theCertificate.getPublicKey());
|
||||
verifier.update(StringUtils.getBytesUtf8(DigestUtils.sha256Hex(data)));
|
||||
|
||||
return verifier.verify(Base64.decodeBase64(signature));
|
||||
}
|
||||
catch(GeneralSecurityException e) {
|
||||
log.error("Unable to verify signature", e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Checks if the certificate has been added to the specified allow file */
|
||||
public boolean isSaved(boolean local) {
|
||||
File allowed = FileUtilities.getFile(Constants.ALLOW_FILE, local);
|
||||
return existsInAnyFile(getFingerprint(), allowed);
|
||||
}
|
||||
|
||||
/** Checks if the certificate has been added to any allow file */
|
||||
public boolean isSaved() {
|
||||
return isSaved(false) || isSaved(true);
|
||||
}
|
||||
|
||||
/** Checks if the certificate has been added to the local block file */
|
||||
public boolean isBlocked() {
|
||||
File blocks = FileUtilities.getFile(Constants.BLOCK_FILE, true);
|
||||
File blocksShared = FileUtilities.getFile(Constants.BLOCK_FILE, false);
|
||||
return existsInAnyFile(getFingerprint(), blocksShared, blocks);
|
||||
}
|
||||
|
||||
private static boolean existsInAnyFile(String fingerprint, File... files) {
|
||||
for(File file : files) {
|
||||
if (file == null) { continue; }
|
||||
|
||||
try(BufferedReader br = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
while((line = br.readLine()) != null) {
|
||||
if (line.contains("\t")) {
|
||||
String print = line.substring(0, line.indexOf("\t"));
|
||||
if (print.equals(fingerprint)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public String getCommonName() {
|
||||
return commonName;
|
||||
}
|
||||
|
||||
public String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public String getValidFrom() {
|
||||
if (validFrom.isAfter(UNKNOWN_MIN)) {
|
||||
return DATE_FORMAT.format(validFrom.atZone(ZoneOffset.UTC));
|
||||
} else {
|
||||
return "Not Provided";
|
||||
}
|
||||
}
|
||||
|
||||
public String getValidTo() {
|
||||
if (validTo.isBefore(UNKNOWN_MAX)) {
|
||||
return DATE_FORMAT.format(validTo.atZone(ZoneOffset.UTC));
|
||||
} else {
|
||||
return "Not Provided";
|
||||
}
|
||||
}
|
||||
|
||||
public Instant getValidFromDate() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public Instant getValidToDate() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates certificate against embedded cert.
|
||||
*/
|
||||
public boolean isTrusted() {
|
||||
return isValid() && !isExpired();
|
||||
}
|
||||
|
||||
public boolean isSponsored() {
|
||||
return sponsored;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return expired;
|
||||
}
|
||||
|
||||
|
||||
public static String makeThumbPrint(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
md.update(cert.getEncoded());
|
||||
return ByteUtilities.bytesToHex(md.digest(), false);
|
||||
}
|
||||
|
||||
private String data(boolean assumeTrusted) {
|
||||
return getFingerprint() + "\t" +
|
||||
getCommonName() + "\t" +
|
||||
getOrganization() + "\t" +
|
||||
getValidFrom() + "\t" +
|
||||
getValidTo() + "\t" +
|
||||
// Used by equals(), may fail if it hasn't been trusted yet
|
||||
(assumeTrusted ? true : isTrusted());
|
||||
}
|
||||
|
||||
public String data() {
|
||||
return data(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getOrganization() + " (" + getCommonName() + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Certificate) {
|
||||
return ((Certificate)obj).data(true).equals(data(true));
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
public static void setTrustBuiltIn(boolean trustBuiltIn) {
|
||||
if(trustBuiltIn) {
|
||||
if (!rootCAs.contains(builtIn)) {
|
||||
log.debug("Adding internal CA certificate: CN={}, O={} ({})",
|
||||
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
|
||||
builtIn.rootCA = true;
|
||||
builtIn.valid = true;
|
||||
rootCAs.add(0, builtIn);
|
||||
}
|
||||
} else {
|
||||
if (rootCAs.contains(builtIn)) {
|
||||
log.debug("Removing internal CA certificate: CN={}, O={} ({})",
|
||||
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
|
||||
rootCAs.remove(builtIn);
|
||||
}
|
||||
}
|
||||
Certificate.trustBuiltIn = trustBuiltIn;
|
||||
}
|
||||
|
||||
public static boolean isTrustBuiltIn() {
|
||||
return trustBuiltIn;
|
||||
}
|
||||
|
||||
public static boolean hasAdditionalCAs() {
|
||||
return rootCAs.size() > (isTrustBuiltIn() ? 1 : 0);
|
||||
}
|
||||
|
||||
private static String getSubjectX509Principal(X509Certificate cert, ASN1ObjectIdentifier key) {
|
||||
try {
|
||||
Vector v = PrincipalUtil.getSubjectX509Principal(cert).getValues(key);
|
||||
if(v.size() > 0) {
|
||||
return String.valueOf(v.get(0));
|
||||
}
|
||||
} catch(CertificateEncodingException e) {
|
||||
log.warn("Certificate encoding exception occurred", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
87
old code/tray/src/qz/auth/PairingAuth.java
Executable file
87
old code/tray/src/qz/auth/PairingAuth.java
Executable file
@@ -0,0 +1,87 @@
|
||||
package qz.auth;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PairingAuth {
|
||||
private static String site = null;
|
||||
private static String pairingKey = null;
|
||||
private static final String HMAC_ALGO = "HmacSHA256";
|
||||
private static File configFile = null;
|
||||
|
||||
static {
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
private static void loadConfig() {
|
||||
try {
|
||||
// Use QZ Tray's user directory - same location as PairingConfigDialog
|
||||
configFile = new File(FileUtilities.USER_DIR.toFile(), "pairing-config.json");
|
||||
|
||||
if (!configFile.exists()) {
|
||||
System.out.println("Pairing config file not found at: " + configFile.getAbsolutePath());
|
||||
site = null;
|
||||
pairingKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
String content = new String(Files.readAllBytes(configFile.toPath()));
|
||||
JSONObject config = new JSONObject(content);
|
||||
site = config.optString("site", null);
|
||||
pairingKey = config.optString("pairing_key", null);
|
||||
|
||||
System.out.println("Pairing config loaded from: " + configFile.getAbsolutePath());
|
||||
System.out.println("Site configured: " + site);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error loading pairing config: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
site = null;
|
||||
pairingKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
public static String getPairingKey() {
|
||||
return pairingKey;
|
||||
}
|
||||
|
||||
public static boolean isConfigured() {
|
||||
return site != null && !site.isEmpty() && pairingKey != null && !pairingKey.isEmpty();
|
||||
}
|
||||
|
||||
public static String getConfigFilePath() {
|
||||
return configFile != null ? configFile.getAbsolutePath() : "unknown";
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
public static String hmacSignature(String message) {
|
||||
if (pairingKey == null || pairingKey.isEmpty()) {
|
||||
System.err.println("Cannot compute HMAC: pairing key is not configured");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGO);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(pairingKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGO);
|
||||
mac.init(keySpec);
|
||||
byte[] hmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hmac);
|
||||
} catch (Exception e) {
|
||||
System.err.println("HMAC computation failed: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
118
old code/tray/src/qz/auth/RequestState.java
Executable file
118
old code/tray/src/qz/auth/RequestState.java
Executable file
@@ -0,0 +1,118 @@
|
||||
package qz.auth;
|
||||
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.common.Constants;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class RequestState {
|
||||
|
||||
public enum Validity {
|
||||
TRUSTED("Valid"),
|
||||
EXPIRED("Expired Signature"),
|
||||
UNSIGNED("Invalid Signature"),
|
||||
EXPIRED_CERT("Expired Certificate"),
|
||||
FUTURE_CERT("Future Certificate"),
|
||||
INVALID_CERT("Invalid Certificate"),
|
||||
UNKNOWN("Invalid");
|
||||
|
||||
private String formatted;
|
||||
|
||||
Validity(String formatted) {
|
||||
this.formatted = formatted;
|
||||
}
|
||||
|
||||
public String getFormatted() {
|
||||
return formatted;
|
||||
}
|
||||
}
|
||||
|
||||
Certificate certUsed;
|
||||
JSONObject requestData;
|
||||
|
||||
boolean initialConnect;
|
||||
Validity status;
|
||||
|
||||
public RequestState(Certificate cert, JSONObject data) {
|
||||
certUsed = cert;
|
||||
requestData = data;
|
||||
status = Validity.UNKNOWN;
|
||||
}
|
||||
|
||||
public Certificate getCertUsed() {
|
||||
return certUsed;
|
||||
}
|
||||
|
||||
public JSONObject getRequestData() {
|
||||
return requestData;
|
||||
}
|
||||
|
||||
public boolean isInitialConnect() {
|
||||
return initialConnect;
|
||||
}
|
||||
|
||||
public void markNewConnection(Certificate cert) {
|
||||
certUsed = cert;
|
||||
initialConnect = true;
|
||||
|
||||
checkCertificateState(cert);
|
||||
}
|
||||
|
||||
public void checkCertificateState(Certificate cert) {
|
||||
if (cert.isTrusted()) {
|
||||
status = Validity.TRUSTED;
|
||||
} else if (cert.getValidToDate().isBefore(Instant.now())) {
|
||||
status = Validity.EXPIRED_CERT;
|
||||
} else if (cert.getValidFromDate().isAfter(Instant.now())) {
|
||||
status = Validity.FUTURE_CERT;
|
||||
} else if (!cert.isValid()) {
|
||||
status = Validity.INVALID_CERT;
|
||||
} else {
|
||||
status = Validity.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public Validity getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Validity state) {
|
||||
status = state;
|
||||
}
|
||||
|
||||
public boolean hasCertificate() {
|
||||
return certUsed != null && certUsed != Certificate.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean hasSavedCert() {
|
||||
return isVerified() && certUsed.isSaved();
|
||||
}
|
||||
|
||||
public boolean hasBlockedCert() {
|
||||
return certUsed == null || certUsed.isBlocked();
|
||||
}
|
||||
|
||||
public String getCertName() {
|
||||
return certUsed.getCommonName();
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
return certUsed.isTrusted() && status == Validity.TRUSTED;
|
||||
}
|
||||
|
||||
public boolean isSponsored() {
|
||||
return certUsed.isSponsored();
|
||||
}
|
||||
|
||||
public String getValidityInfo() {
|
||||
if (status == Validity.TRUSTED) {
|
||||
return Constants.TRUSTED_CERT;
|
||||
} else if (Arrays.asList(Validity.UNSIGNED, Validity.EXPIRED, Validity.EXPIRED_CERT, Validity.FUTURE_CERT).contains(status)) {
|
||||
return Constants.NO_TRUST + " - " + status.getFormatted();
|
||||
} else {
|
||||
return Constants.UNTRUSTED_CERT;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
12
old code/tray/src/qz/auth/X509Constants.java
Executable file
12
old code/tray/src/qz/auth/X509Constants.java
Executable file
@@ -0,0 +1,12 @@
|
||||
package qz.auth;
|
||||
|
||||
/**
|
||||
* Created by Tres on 3/3/2015.
|
||||
*/
|
||||
public class X509Constants {
|
||||
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
|
||||
public static final String END_CERT = "-----END CERTIFICATE-----";
|
||||
public static final String INTERMEDIATE_CERT = "--START INTERMEDIATE CERT--";
|
||||
public static final String BEGIN_CRL = "-----BEGIN X509 CRL-----";
|
||||
public static final String END_CRL = "-----END X509 CRL-----";
|
||||
}
|
||||
129
old code/tray/src/qz/build/Fetcher.java
Executable file
129
old code/tray/src/qz/build/Fetcher.java
Executable file
@@ -0,0 +1,129 @@
|
||||
package qz.build;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* Fetches a zip or tarball from URL and decompresses it
|
||||
*/
|
||||
public class Fetcher {
|
||||
public enum Format {
|
||||
ZIP(".zip"),
|
||||
TARBALL(".tar.gz"),
|
||||
UNKNOWN(null);
|
||||
|
||||
String suffix;
|
||||
Format(String suffix) {
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
public String getSuffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
public static Format parse(String url) {
|
||||
for(Format format : Format.values()) {
|
||||
if (url.endsWith(format.getSuffix())) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger log = LogManager.getLogger(Fetcher.class);
|
||||
|
||||
public static void main(String ... args) throws IOException {
|
||||
new Fetcher("jlink/qz-tray-src_x.x.x", "https://github.com/qzind/tray/archive/master.tar.gz").fetch().uncompress();
|
||||
}
|
||||
|
||||
String resourceName;
|
||||
String url;
|
||||
Format format;
|
||||
Path rootDir;
|
||||
File tempArchive;
|
||||
File tempExtracted;
|
||||
File extracted;
|
||||
|
||||
public Fetcher(String resourceName, String url) {
|
||||
this.url = url;
|
||||
this.resourceName = resourceName;
|
||||
this.format = Format.parse(url);
|
||||
// Try to calculate out/
|
||||
this.rootDir = SystemUtilities.getJarParentPath().getParent();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Fetcher(String resourceName, String url, Format format, String rootDir) {
|
||||
this.resourceName = resourceName;
|
||||
this.url = url;
|
||||
this.format = format;
|
||||
this.rootDir = Paths.get(rootDir);
|
||||
}
|
||||
|
||||
public Fetcher fetch() throws IOException {
|
||||
extracted = new File(rootDir.toString(), resourceName);
|
||||
if(extracted.isDirectory() && extracted.exists()) {
|
||||
log.info("Resource '{}' from [{}] has already been downloaded and extracted. Using: [{}]", resourceName, url, extracted);
|
||||
} else {
|
||||
tempExtracted = new File(rootDir.toString(), resourceName + "~tmp");
|
||||
if(tempExtracted.exists()) {
|
||||
FileUtils.deleteDirectory(tempExtracted);
|
||||
}
|
||||
// temp directory to thwart partial extraction
|
||||
tempExtracted.mkdirs();
|
||||
tempArchive = File.createTempFile(resourceName, ".zip");
|
||||
log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempArchive);
|
||||
FileUtils.copyURLToFile(new URL(url), tempArchive);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String uncompress() throws IOException {
|
||||
if(tempArchive != null) {
|
||||
log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempArchive, tempExtracted);
|
||||
if(format == Format.ZIP) {
|
||||
unzip(tempArchive.getAbsolutePath(), tempExtracted);
|
||||
} else {
|
||||
untar(tempArchive.getAbsolutePath(), tempExtracted);
|
||||
}
|
||||
log.info("Moving [{}] to [{}]", tempExtracted, extracted);
|
||||
tempExtracted.renameTo(extracted);
|
||||
}
|
||||
return extracted.toString();
|
||||
}
|
||||
|
||||
public static void untar(String sourceFile, File targetDir) throws IOException {
|
||||
// TODO: Switch to TarArchiveInputStream from Apache Commons Compress
|
||||
if (!ShellUtilities.execute("tar", "-xzf", sourceFile, "-C", targetDir.getPath())) {
|
||||
throw new IOException("Something went wrong extracting " + sourceFile +", check logs for details");
|
||||
}
|
||||
}
|
||||
|
||||
public static void unzip(String sourceFile, File targetDir) throws IOException {
|
||||
try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(sourceFile))) {
|
||||
for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
|
||||
Path resolvedPath = targetDir.toPath().resolve(ze.getName());
|
||||
if (ze.isDirectory()) {
|
||||
Files.createDirectories(resolvedPath);
|
||||
} else {
|
||||
Files.createDirectories(resolvedPath.getParent());
|
||||
Files.copy(zipIn, resolvedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
341
old code/tray/src/qz/build/JLink.java
Executable file
341
old code/tray/src/qz/build/JLink.java
Executable file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2020 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.build;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.build.jlink.Platform;
|
||||
import qz.build.jlink.Vendor;
|
||||
import qz.build.jlink.Url;
|
||||
import qz.build.provision.params.Arch;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class JLink {
|
||||
private static final Logger log = LogManager.getLogger(JLink.class);
|
||||
public static final Vendor JAVA_DEFAULT_VENDOR = Vendor.BELLSOFT;
|
||||
private static final String JAVA_DEFAULT_VERSION = "11.0.17+7";
|
||||
private static final String JAVA_DEFAULT_GC_ENGINE = "hotspot"; // or "openj9"
|
||||
private static final String JAVA_DEFAULT_GC_VERSION = "0.35.0"; // openj9 gc only
|
||||
|
||||
private Path jarPath;
|
||||
private Path jdepsPath;
|
||||
private Path jlinkPath;
|
||||
private Path jmodsPath;
|
||||
private Path outPath;
|
||||
private Version jdepsVersion;
|
||||
private Platform hostPlatform;
|
||||
private Platform targetPlatform;
|
||||
private Arch hostArch;
|
||||
private Arch targetArch;
|
||||
private Vendor javaVendor;
|
||||
private String gcEngine;
|
||||
private String javaVersion;
|
||||
private String gcVersion;
|
||||
|
||||
private Path targetJdk;
|
||||
|
||||
private Version javaSemver;
|
||||
|
||||
private LinkedHashSet<String> depList;
|
||||
|
||||
public JLink(String targetPlatform, String targetArch, String javaVendor, String javaVersion, String gcEngine, String gcVersion, String targetJdk) throws IOException {
|
||||
this.hostPlatform = Platform.getCurrentPlatform();
|
||||
this.hostArch = SystemUtilities.getArch();
|
||||
|
||||
this.targetPlatform = Platform.parse(targetPlatform, this.hostPlatform);
|
||||
this.targetArch = Arch.parse(targetArch, this.hostArch);
|
||||
this.javaVendor = Vendor.parse(javaVendor, JAVA_DEFAULT_VENDOR);
|
||||
this.gcEngine = getParam("gcEngine", gcEngine, JAVA_DEFAULT_GC_ENGINE);
|
||||
this.javaVersion = getParam("javaVersion", javaVersion, JAVA_DEFAULT_VERSION);
|
||||
this.gcVersion = getParam("gcVersion", gcVersion, JAVA_DEFAULT_GC_VERSION);
|
||||
|
||||
this.javaSemver = SystemUtilities.getJavaVersion(this.javaVersion);
|
||||
|
||||
// Optional: Provide the location of a custom JDK on the local filesystem
|
||||
if(!StringUtils.isEmpty(targetJdk)) {
|
||||
Path jdkPath = Paths.get(targetJdk);
|
||||
Properties jdkProps = new Properties();
|
||||
jdkProps.load(new FileInputStream(jdkPath.resolve("release").toFile()));
|
||||
String customVersion = jdkProps.getProperty("JAVA_VERSION");
|
||||
if(customVersion.contains("\"")) {
|
||||
customVersion = customVersion.split("\"")[1];
|
||||
}
|
||||
Version customSemver = SystemUtilities.getJavaVersion(customVersion);
|
||||
if(needsDownload(javaSemver, customSemver)) {
|
||||
// The "release" file doesn't have build info, so we can't auto-download :(
|
||||
if(javaSemver.getMajorVersion() != customSemver.getMajorVersion()) {
|
||||
log.error("Error: jlink version {}.0 does not match target java.base version {}.0", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
|
||||
} else {
|
||||
// Handle edge-cases (e.g. JDK-8240734)
|
||||
log.error("Error: jlink version {} is incompatible with target java.base version {}", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
|
||||
}
|
||||
System.exit(2);
|
||||
}
|
||||
this.targetJdk = Paths.get(targetJdk);
|
||||
calculateToolPaths(Paths.get(targetJdk));
|
||||
} else {
|
||||
// Determine if the version we're building with is compatible with the target version
|
||||
if (needsDownload(javaSemver, Constants.JAVA_VERSION)) {
|
||||
log.warn("Java versions are incompatible, locating a suitable runtime for Java " + javaSemver.getMajorVersion() + "...");
|
||||
String hostJdk = downloadJdk(this.hostArch, this.hostPlatform);
|
||||
calculateToolPaths(Paths.get(hostJdk));
|
||||
} else {
|
||||
calculateToolPaths(null);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.targetJdk == null) {
|
||||
targetJdk = downloadJdk(this.targetArch, this.targetPlatform);
|
||||
jmodsPath = Paths.get(targetJdk, "jmods");
|
||||
} else {
|
||||
log.info("\"targetjdk\" was provided {}, skipping download", targetJdk);
|
||||
jmodsPath = this.targetJdk.resolve("jmods");
|
||||
}
|
||||
|
||||
log.info("Selecting jmods folder: {}", jmodsPath);
|
||||
|
||||
calculateJarPath()
|
||||
.calculateOutPath()
|
||||
.calculateDepList()
|
||||
.deployJre();
|
||||
}
|
||||
|
||||
public static void main(String ... args) throws IOException {
|
||||
new JLink(null, null, null, null, null, null, null).calculateJarPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incompatibilities between JDKs, download a fresh one if needed
|
||||
*/
|
||||
private static boolean needsDownload(Version want, Version installed) {
|
||||
// jdeps and jlink historically require matching major JDK versions. Download if needed.
|
||||
boolean downloadJdk = installed.getMajorVersion() != want.getMajorVersion();
|
||||
|
||||
// Per JDK-8240734: Major versions checks aren't enough starting with 11.0.16+8
|
||||
// see also https://github.com/adoptium/adoptium-support/issues/557
|
||||
Version bad = SystemUtilities.getJavaVersion("11.0.16+8");
|
||||
if(want.greaterThanOrEqualTo(bad) && installed.lessThan(bad) ||
|
||||
installed.greaterThanOrEqualTo(bad) && want.lessThan(bad)) {
|
||||
// Force download
|
||||
// Fixes "Hash of java.rmi differs from expected hash"
|
||||
downloadJdk = true;
|
||||
}
|
||||
return downloadJdk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the JDK and return the path it was extracted to
|
||||
*/
|
||||
private String downloadJdk(Arch arch, Platform platform) throws IOException {
|
||||
String url = new Url(this.javaVendor).format(arch, platform, this.gcEngine, this.javaSemver, this.javaVersion, this.gcVersion);
|
||||
|
||||
// Saves to out e.g. "out/jlink/jdk-AdoptOpenjdk-amd64-platform-11_0_7"
|
||||
String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.value(), arch, platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url)
|
||||
.fetch()
|
||||
.uncompress();
|
||||
|
||||
// Get first subfolder, e.g. jdk-11.0.7+10
|
||||
for(File subfolder : new File(extractedJdk).listFiles(pathname -> pathname.isDirectory())) {
|
||||
extractedJdk = subfolder.getPath();
|
||||
if(platform == Platform.MAC && Paths.get(extractedJdk, "/Contents/Home").toFile().isDirectory()) {
|
||||
extractedJdk += "/Contents/Home";
|
||||
}
|
||||
log.info("Selecting JDK home: {}", extractedJdk);
|
||||
break;
|
||||
}
|
||||
|
||||
return extractedJdk;
|
||||
}
|
||||
|
||||
private JLink calculateJarPath() {
|
||||
if(SystemUtilities.isJar()) {
|
||||
jarPath = SystemUtilities.getJarPath();
|
||||
} else {
|
||||
// Detect out/dist/qz-tray.jar for IDE usage
|
||||
jarPath = SystemUtilities.getJarParentPath()
|
||||
.resolve("dist")
|
||||
.resolve(Constants.PROPS_FILE + ".jar");
|
||||
}
|
||||
log.info("Assuming jar path: {}", jarPath);
|
||||
return this;
|
||||
}
|
||||
|
||||
private JLink calculateOutPath() {
|
||||
switch(targetPlatform) {
|
||||
case MAC:
|
||||
outPath = jarPath.resolve("../Java.runtime/Contents/Home").normalize();
|
||||
break;
|
||||
default:
|
||||
outPath = jarPath.resolve("../runtime").normalize();
|
||||
}
|
||||
log.info("Assuming output path: {}", outPath);
|
||||
return this;
|
||||
}
|
||||
|
||||
private JLink calculateToolPaths(Path javaHome) throws IOException {
|
||||
if(javaHome == null) {
|
||||
javaHome = Paths.get(System.getProperty("java.home"));
|
||||
}
|
||||
log.info("Using JAVA_HOME: {}", javaHome);
|
||||
jdepsPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jdeps.exe" : "jdeps").normalize();
|
||||
jlinkPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jlink.exe" : "jlink").normalize();
|
||||
log.info("Assuming jdeps path: {}", jdepsPath);
|
||||
log.info("Assuming jlink path: {}", jlinkPath);
|
||||
jdepsPath.toFile().setExecutable(true, false);
|
||||
jlinkPath.toFile().setExecutable(true, false);
|
||||
jdepsVersion = SystemUtilities.getJavaVersion(jdepsPath);
|
||||
return this;
|
||||
}
|
||||
|
||||
private JLink calculateDepList() throws IOException {
|
||||
log.info("Calling jdeps to determine runtime dependencies");
|
||||
depList = new LinkedHashSet<>();
|
||||
|
||||
// JDK11.0.11+requires suppressing of missing deps
|
||||
String raw = jdepsVersion.compareTo(Version.valueOf("11.0.10")) > 0 ?
|
||||
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", "--ignore-missing-deps", jarPath.toString()) :
|
||||
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", jarPath.toString());
|
||||
if (raw == null || raw.trim().isEmpty() || raw.trim().startsWith("Warning") ) {
|
||||
throw new IOException("An unexpected error occurred calling jdeps. Please check the logs for details.\n" + raw);
|
||||
}
|
||||
for(String item : raw.split("\\r?\\n")) {
|
||||
item = item.trim();
|
||||
if(!item.isEmpty()) {
|
||||
if(item.startsWith("JDK") || item.startsWith("jdk8internals")) {
|
||||
// Remove e.g. "JDK removed internal API/sun.reflect"
|
||||
log.trace("Removing dependency: '{}'", item);
|
||||
continue;
|
||||
}
|
||||
if(item.contains("/")) {
|
||||
// Isolate base name e.g. "java.base/com.sun.net.ssl"
|
||||
item = item.split("/")[0];
|
||||
}
|
||||
depList.add(item);
|
||||
}
|
||||
}
|
||||
switch(targetPlatform) {
|
||||
case WINDOWS:
|
||||
// Java accessibility bridge dependency, see https://github.com/qzind/tray/issues/1234
|
||||
depList.add("jdk.accessibility");
|
||||
default:
|
||||
// Adds "bin/jcmd"
|
||||
depList.add("jdk.jcmd");
|
||||
// "jar:" URLs create transient zipfs dependency, see https://stackoverflow.com/a/57846672/3196753
|
||||
depList.add("jdk.zipfs");
|
||||
// fix for https://github.com/qzind/tray/issues/894 solution from https://github.com/adoptium/adoptium-support/issues/397
|
||||
depList.add("jdk.crypto.ec");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private JLink deployJre() throws IOException {
|
||||
if(targetPlatform == Platform.MAC) {
|
||||
// Deploy Contents/MacOS/libjli.dylib
|
||||
Path macOS = Files.createDirectories(outPath.resolve("../MacOS").normalize());
|
||||
Path jliLib = macOS.resolve("libjli.dylib");
|
||||
log.info("Deploying {}", macOS);
|
||||
try {
|
||||
// Not all jdks use a bundle format, but try this first
|
||||
Files.copy(jmodsPath.resolve("../../MacOS/libjli.dylib").normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch(IOException ignore) {
|
||||
// Fallback to flat format
|
||||
String libjli = "../lib/jli/libjli.dylib";
|
||||
if(javaSemver.getMajorVersion() >= 21) {
|
||||
libjli = "../lib/libjli.dylib";
|
||||
}
|
||||
Files.copy(jmodsPath.resolve(libjli).normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
// Deploy Contents/Info.plist
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
fieldMap.put("%BUNDLE_ID%", MacUtilities.getBundleId() + ".jre"); // e.g. io.qz.qz-tray.jre
|
||||
fieldMap.put("%BUNDLE_VERSION%", String.format("%s.%s.%s", javaSemver.getMajorVersion(), javaSemver.getMinorVersion(), javaSemver.getPatchVersion()));
|
||||
fieldMap.put("%BUNDLE_VERSION_FULL%", javaSemver.toString());
|
||||
fieldMap.put("%BUNDLE_VENDOR%", javaVendor.getVendorName());
|
||||
fieldMap.put("%BUNDLE_PRODUCT%", javaVendor.getProductName());
|
||||
log.info("Deploying {}/Info.plist", macOS.getParent());
|
||||
FileUtilities.configureAssetFile("assets/mac-runtime.plist.in", macOS.getParent().resolve("Info.plist"), fieldMap, JLink.class);
|
||||
}
|
||||
|
||||
FileUtils.deleteQuietly(outPath.toFile());
|
||||
|
||||
if(ShellUtilities.execute(jlinkPath.toString(),
|
||||
"--strip-debug",
|
||||
"--compress=2",
|
||||
"--no-header-files",
|
||||
"--no-man-pages",
|
||||
"--exclude-files=glob:**/legal/**",
|
||||
"--module-path", jmodsPath.toString(),
|
||||
"--add-modules", String.join(",", depList),
|
||||
"--output", outPath.toString())) {
|
||||
log.info("Successfully deployed a jre to {}", outPath);
|
||||
|
||||
// Remove all but java/javaw
|
||||
List<String> keepFiles = new ArrayList<>();
|
||||
//String[] keepFiles;
|
||||
String keepExt;
|
||||
switch(targetPlatform) {
|
||||
case WINDOWS:
|
||||
keepFiles.add("java.exe");
|
||||
keepFiles.add("javaw.exe");
|
||||
keepFiles.add("jcmd.exe");
|
||||
if(depList.contains("jdk.accessibility")) {
|
||||
// Java accessibility bridge switching tool
|
||||
keepFiles.add("jabswitch.exe");
|
||||
}
|
||||
// Windows stores ".dll" files in bin
|
||||
keepExt = ".dll";
|
||||
break;
|
||||
default:
|
||||
keepFiles.add("java");
|
||||
keepFiles.add("jcmd");
|
||||
keepExt = null;
|
||||
}
|
||||
|
||||
Files.list(outPath.resolve("bin")).forEach(binFile -> {
|
||||
if(Files.isDirectory(binFile) || (keepExt != null && binFile.toString().endsWith(keepExt))) {
|
||||
log.info("Keeping {}", binFile);
|
||||
return; // iterate forEach
|
||||
}
|
||||
for(String name : keepFiles) {
|
||||
if (binFile.endsWith(name)) {
|
||||
log.info("Keeping {}", binFile);
|
||||
return; // iterate forEach
|
||||
}
|
||||
}
|
||||
log.info("Deleting {}", binFile);
|
||||
binFile.toFile().delete();
|
||||
});
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
throw new IOException("An error occurred deploying the jre. Please check the logs for details.");
|
||||
}
|
||||
|
||||
public static String getParam(String paramName, String value, String fallback) {
|
||||
if(value != null && !value.isEmpty() && !value.trim().isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
log.info("No {} specified, assuming '{}'", paramName, fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
45
old code/tray/src/qz/build/assets/mac-runtime.plist.in
Executable file
45
old code/tray/src/qz/build/assets/mac-runtime.plist.in
Executable file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>libjli.dylib</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>%BUNDLE_VENDOR% %BUNDLE_PRODUCT% %BUNDLE_VERSION_FULL%</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%BUNDLE_ID%</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>7.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Java Runtime Image</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>%BUNDLE_VERSION%</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>%BUNDLE_VERSION%</string>
|
||||
<key>JavaVM</key>
|
||||
<dict>
|
||||
<key>JVMCapabilities</key>
|
||||
<array>
|
||||
<string>CommandLine</string>
|
||||
<string>JNI</string>
|
||||
<string>BundledApp</string>
|
||||
</array>
|
||||
<key>JVMMinimumFrameworkVersion</key>
|
||||
<string>17.0.0</string>
|
||||
<key>JVMMinimumSystemVersion</key>
|
||||
<string>10.6.0</string>
|
||||
<key>JVMPlatformVersion</key>
|
||||
<string>%BUNDLE_VERSION_FULL%</string>
|
||||
<key>JVMVendor</key>
|
||||
<string>%BUNDLE_VENDOR%</string>
|
||||
<key>JVMVersion</key>
|
||||
<string>%BUNDLE_VERSION%</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
0
old code/tray/src/qz/build/jlink/Arch.java
Executable file
0
old code/tray/src/qz/build/jlink/Arch.java
Executable file
68
old code/tray/src/qz/build/jlink/Parsable.java
Executable file
68
old code/tray/src/qz/build/jlink/Parsable.java
Executable file
@@ -0,0 +1,68 @@
|
||||
package qz.build.jlink;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A special template class for handling enums with varargs needing string matches.
|
||||
*
|
||||
* Parsable enums must declare <code>public static void String[] matches;</code>
|
||||
* in the constructor, which <code>parse(Class enumType, </T>String value) will
|
||||
* call using reflection.
|
||||
*
|
||||
* Enums are inherently static in Java and cannot extend superclasses. The
|
||||
* workaround to avoid code duplication is to leverage reflection and generics in
|
||||
* static utility functions.
|
||||
*
|
||||
* The downsides of this are:
|
||||
* - Reflection is slow
|
||||
* - Static helpers must be explicitly class-type-aware*
|
||||
*
|
||||
* *Non-static methods may be implicit, but create anti-patterns for static helpers
|
||||
* such as <code>parse(String value)</code> as they would exist at
|
||||
* <code>ENUM_ENTRY.parse(...)</code> rather than <code>EnumClass.parse(...)</code>.
|
||||
*/
|
||||
public interface Parsable<T extends Enum> {
|
||||
Logger log = LogManager.getLogger(Parsable.class);
|
||||
|
||||
static <T extends Enum<T>> T parse(Class<T> enumType, String value) {
|
||||
if(value != null && !value.trim().isEmpty()) {
|
||||
for(T parsable : enumType.getEnumConstants()) {
|
||||
try {
|
||||
Field matchesField = parsable.getClass().getDeclaredField("matches");
|
||||
String[] matches = (String[])matchesField.get(parsable);
|
||||
for(String match : matches) {
|
||||
if (match.equalsIgnoreCase(value)) {
|
||||
return parsable;
|
||||
}
|
||||
}
|
||||
} catch(NoSuchFieldException | IllegalAccessException | ClassCastException e) {
|
||||
log.warn("Parsable enums must have a 'public String[] matches' field", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("Could not parse {} as a valid {} value", value, enumType.getSimpleName());
|
||||
return null;
|
||||
}
|
||||
|
||||
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback, boolean silent) {
|
||||
if(value != null && !value.trim().isEmpty()) {
|
||||
return parse(enumType, value);
|
||||
}
|
||||
if(!silent) {
|
||||
log.warn("No {} specified, assuming '{}'", enumType.getSimpleName(), ((Parsable)fallback).value());
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback) {
|
||||
return parse(enumType, value, fallback, false);
|
||||
}
|
||||
|
||||
default String value() {
|
||||
return ((T)this).toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
37
old code/tray/src/qz/build/jlink/Platform.java
Executable file
37
old code/tray/src/qz/build/jlink/Platform.java
Executable file
@@ -0,0 +1,37 @@
|
||||
package qz.build.jlink;
|
||||
|
||||
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
/**
|
||||
* Handling of platform names as they would appear in a URL
|
||||
* Values added must also be added to <code>ArgValue.JLINK --platform</code> values
|
||||
*/
|
||||
public enum Platform implements Parsable {
|
||||
MAC("mac"),
|
||||
WINDOWS("windows"),
|
||||
LINUX("linux");
|
||||
|
||||
public final String[] matches;
|
||||
Platform(String ... matches) { this.matches = matches; }
|
||||
|
||||
public static Platform parse(String value, Platform fallback) {
|
||||
return Parsable.parse(Platform.class, value, fallback);
|
||||
}
|
||||
|
||||
public static Platform parse(String value) {
|
||||
return Parsable.parse(Platform.class, value);
|
||||
}
|
||||
|
||||
public static Platform getCurrentPlatform() {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case MAC:
|
||||
return Platform.MAC;
|
||||
case WINDOWS:
|
||||
return Platform.WINDOWS;
|
||||
case LINUX:
|
||||
default:
|
||||
return Platform.LINUX;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
old code/tray/src/qz/build/jlink/Url.java
Executable file
72
old code/tray/src/qz/build/jlink/Url.java
Executable file
@@ -0,0 +1,72 @@
|
||||
package qz.build.jlink;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.build.provision.params.Arch;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static qz.build.jlink.Vendor.*;
|
||||
|
||||
/**
|
||||
* Each JDK provider uses their own url format
|
||||
*/
|
||||
public class Url {
|
||||
static HashMap<Vendor, String> VENDOR_URL_MAP = new HashMap<>();
|
||||
static {
|
||||
VENDOR_URL_MAP.put(BELLSOFT, "https://download.bell-sw.com/java/%s/bellsoft-jdk%s-%s-%s.%s");
|
||||
VENDOR_URL_MAP.put(ECLIPSE, "https://github.com/adoptium/temurin%s-binaries/releases/download/jdk-%s/OpenJDK%sU-jdk_%s_%s_%s_%s.%s");
|
||||
VENDOR_URL_MAP.put(IBM, "https://github.com/ibmruntimes/semeru%s-binaries/releases/download/jdk-%s_%s-%s/ibm-semeru-open-jdk_%s_%s_%s_%s-%s.%s");
|
||||
VENDOR_URL_MAP.put(MICROSOFT, "https://aka.ms/download-jdk/microsoft-jdk-%s-%s-%s.%s");
|
||||
VENDOR_URL_MAP.put(AMAZON, "https://corretto.aws/downloads/resources/%s/amazon-corretto-%s-%s-%s.%s");
|
||||
VENDOR_URL_MAP.put(AZUL, "https://cdn.azul.com/zulu%s/bin/zulu%s-ca-jdk%s-%s_%s.%s");
|
||||
}
|
||||
|
||||
private static final Logger log = LogManager.getLogger(Url.class);
|
||||
|
||||
Vendor vendor;
|
||||
String pattern;
|
||||
public Url(Vendor vendor) {
|
||||
this.vendor = vendor;
|
||||
if(!VENDOR_URL_MAP.containsKey(vendor)) {
|
||||
throw new UnsupportedOperationException(String.format("Vendor provided '%s' couldn't be matched to a URL pattern, aborting.", vendor));
|
||||
}
|
||||
pattern = VENDOR_URL_MAP.get(vendor);
|
||||
}
|
||||
|
||||
public String format(Arch arch, Platform platform, String gcEngine, Version javaSemver, String javaVersion, String gcVer) throws UnsupportedEncodingException {
|
||||
Url pattern = new Url(vendor);
|
||||
String urlArch = vendor.getUrlArch(arch);
|
||||
String fileExt = vendor.getUrlExtension(platform);
|
||||
String urlPlatform = vendor.getUrlPlatform(platform);
|
||||
String urlJavaVersion = vendor.getUrlJavaVersion(javaSemver);
|
||||
|
||||
// Convert "+" to "%2B"
|
||||
String urlJavaVersionEncode = URLEncoder.encode(javaVersion, "UTF-8");
|
||||
|
||||
int javaMajor = javaSemver.getMajorVersion();
|
||||
|
||||
switch(vendor) {
|
||||
case BELLSOFT:
|
||||
return String.format(pattern.pattern, urlJavaVersionEncode, urlJavaVersionEncode, urlPlatform, urlArch, fileExt);
|
||||
case ECLIPSE:
|
||||
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, javaMajor, urlArch, urlPlatform, gcEngine, urlJavaVersion, fileExt);
|
||||
case IBM:
|
||||
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, gcEngine, gcVer, urlArch, urlPlatform, urlJavaVersion, gcEngine, gcVer, fileExt);
|
||||
case MICROSOFT:
|
||||
return String.format(pattern.pattern, urlJavaVersion, urlPlatform, urlArch, fileExt);
|
||||
case AMAZON:
|
||||
return String.format(pattern.pattern, urlJavaVersion, urlJavaVersion, urlPlatform, urlArch, fileExt);
|
||||
case AZUL:
|
||||
// Special handling of Linux aarch64
|
||||
String embedded = platform == Platform.LINUX ? "-embedded" : "";
|
||||
return String.format(pattern.pattern, embedded, gcVer, urlJavaVersion, urlPlatform, urlArch, fileExt);
|
||||
default:
|
||||
throw new UnsupportedOperationException(String.format("URL pattern for '%s' (%s) is missing a format implementation.", vendor, pattern));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
155
old code/tray/src/qz/build/jlink/Vendor.java
Executable file
155
old code/tray/src/qz/build/jlink/Vendor.java
Executable file
@@ -0,0 +1,155 @@
|
||||
package qz.build.jlink;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import qz.build.provision.params.Arch;
|
||||
|
||||
/**
|
||||
* Handling of java vendors
|
||||
*/
|
||||
public enum Vendor implements Parsable {
|
||||
ECLIPSE("Eclipse", "Adoptium", "adoptium", "temurin", "adoptopenjdk"),
|
||||
BELLSOFT("BellSoft", "Liberica", "bellsoft", "liberica"),
|
||||
IBM("IBM", "Semeru", "ibm", "semeru"),
|
||||
MICROSOFT("Microsoft", "OpenJDK", "microsoft"),
|
||||
AMAZON("Amazon", "Corretto", "amazon", "corretto"),
|
||||
AZUL("Azul", "Zulu", "azul", "zulu");
|
||||
|
||||
public String vendorName;
|
||||
public String productName;
|
||||
public final String[] matches;
|
||||
Vendor(String vendorName, String productName, String ... matches) {
|
||||
this.matches = matches;
|
||||
this.vendorName = vendorName;
|
||||
this.productName = productName;
|
||||
}
|
||||
|
||||
public static Vendor parse(String value, Vendor fallback) {
|
||||
return Parsable.parse(Vendor.class, value, fallback, true);
|
||||
}
|
||||
|
||||
public static Vendor parse(String value) {
|
||||
return Parsable.parse(Vendor.class, value);
|
||||
}
|
||||
|
||||
public String getVendorName() {
|
||||
return vendorName;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
return productName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Vendor to Arch value
|
||||
*/
|
||||
public String getUrlArch(Arch arch) {
|
||||
switch(arch) {
|
||||
case AARCH64:
|
||||
// All vendors seem to use "aarch64" universally
|
||||
return "aarch64";
|
||||
case ARM32:
|
||||
switch(this) {
|
||||
case BELLSOFT:
|
||||
return "arm32-vfp-hflt";
|
||||
case AZUL:
|
||||
return "aarch32hf";
|
||||
case MICROSOFT:
|
||||
case IBM:
|
||||
throw new UnsupportedOperationException("Vendor does not provide builds for this architecture");
|
||||
case AMAZON:
|
||||
case ECLIPSE:
|
||||
default:
|
||||
return "arm";
|
||||
}
|
||||
case RISCV64:
|
||||
return "riscv64";
|
||||
case X86:
|
||||
switch(this) {
|
||||
case AZUL:
|
||||
return "i686";
|
||||
case BELLSOFT:
|
||||
return "i586";
|
||||
case ECLIPSE:
|
||||
case IBM:
|
||||
return "x86-32";
|
||||
case AMAZON:
|
||||
default:
|
||||
return "x86";
|
||||
}
|
||||
case X86_64:
|
||||
default:
|
||||
switch(this) {
|
||||
// BellSoft uses "amd64"
|
||||
case BELLSOFT:
|
||||
return "amd64";
|
||||
}
|
||||
return "x64";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Vendor to Platform name
|
||||
*/
|
||||
public String getUrlPlatform(Platform platform) {
|
||||
switch(platform) {
|
||||
case MAC:
|
||||
switch(this) {
|
||||
case BELLSOFT:
|
||||
return "macos";
|
||||
case MICROSOFT:
|
||||
return "macOS";
|
||||
case AMAZON:
|
||||
case AZUL:
|
||||
return "macosx";
|
||||
}
|
||||
default:
|
||||
return platform.value();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Vendor and Platform to file extension
|
||||
*/
|
||||
public String getUrlExtension(Platform platform) {
|
||||
switch(this) {
|
||||
case BELLSOFT:
|
||||
switch(platform) {
|
||||
case LINUX:
|
||||
return "tar.gz";
|
||||
default:
|
||||
// BellSoft uses "zip" for mac and windows platforms
|
||||
return "zip";
|
||||
}
|
||||
default:
|
||||
switch(platform) {
|
||||
case WINDOWS:
|
||||
return "zip";
|
||||
default:
|
||||
return "tar.gz";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrlJavaVersion(Version javaSemver) {
|
||||
switch(this) {
|
||||
case MICROSOFT:
|
||||
case AZUL:
|
||||
// Return shorted version (Microsoft, Azul suppresses the build information from URLs)
|
||||
return javaSemver.toString().split("\\+")[0];
|
||||
case AMAZON:
|
||||
// Return lengthened version (Corretto formats major.minor.patch.build.number, e.g. 11.0.17.8.1)
|
||||
String[] parts = javaSemver.toString().split("\\+");
|
||||
String javaVersion = parts[0];
|
||||
//
|
||||
String buildAndNumber = parts[1];
|
||||
if(!buildAndNumber.contains(".")) {
|
||||
// Append ".1" if ".number" is missing
|
||||
buildAndNumber += ".1";
|
||||
}
|
||||
return String.format("%s.%s", javaVersion, buildAndNumber);
|
||||
}
|
||||
// All others seem to prefer "+" replaced with "_"
|
||||
return javaSemver.toString().replaceAll("\\+", "_");
|
||||
}
|
||||
}
|
||||
|
||||
328
old code/tray/src/qz/build/provision/ProvisionBuilder.java
Executable file
328
old code/tray/src/qz/build/provision/ProvisionBuilder.java
Executable file
@@ -0,0 +1,328 @@
|
||||
package qz.build.provision;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.build.provision.params.Arch;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.build.provision.params.Type;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.provision.invoker.PropertyInvoker;
|
||||
import qz.utils.ArgValue;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ProvisionBuilder {
|
||||
protected static final Logger log = LogManager.getLogger(ProvisionBuilder.class);
|
||||
|
||||
public static final Path BUILD_PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR);
|
||||
public static final File BUILD_PROVISION_FILE = BUILD_PROVISION_FOLDER.resolve(Constants.PROVISION_FILE).toFile();
|
||||
|
||||
private File ingestFile;
|
||||
private JSONArray jsonSteps;
|
||||
private Arch targetArch;
|
||||
private Os targetOs;
|
||||
|
||||
/**
|
||||
* Parses command line input to create a "provision" folder in the dist directory for customizing the installation or startup
|
||||
*/
|
||||
public ProvisionBuilder(String type, String phase, String os, String arch, String data, String args, String description, String ... varArgs) throws IOException, JSONException {
|
||||
createProvisionDirectory(false);
|
||||
|
||||
targetOs = Os.ALL;
|
||||
targetArch = Arch.ALL;
|
||||
jsonSteps = new JSONArray();
|
||||
|
||||
// Wrap into JSON so that we can save it
|
||||
JSONObject jsonStep = new JSONObject();
|
||||
putPattern(jsonStep, "description", description);
|
||||
putPattern(jsonStep, "type", type);
|
||||
putPattern(jsonStep, "phase", phase);
|
||||
putPattern(jsonStep, "os", os);
|
||||
putPattern(jsonStep, "arch", arch);
|
||||
putPattern(jsonStep, "data", data);
|
||||
putPattern(jsonStep, "args", args);
|
||||
putPattern(jsonStep, "arg%d", varArgs);
|
||||
|
||||
// Command line invocation, use the working directory
|
||||
Path relativePath = Paths.get(System.getProperty("user.dir"));
|
||||
ingestStep(jsonStep, relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called by ant's <code>provision</code> target
|
||||
*/
|
||||
public ProvisionBuilder(File antJsonFile, String antTargetOs, String antTargetArch) throws IOException, JSONException {
|
||||
createProvisionDirectory(true);
|
||||
|
||||
// Calculate the target os, architecture
|
||||
this.targetArch = Arch.parseStrict(antTargetArch);
|
||||
this.targetOs = Os.parseStrict(antTargetOs);
|
||||
|
||||
this.jsonSteps = new JSONArray();
|
||||
this.ingestFile = antJsonFile;
|
||||
|
||||
String jsonData = FileUtils.readFileToString(antJsonFile, StandardCharsets.UTF_8);
|
||||
JSONArray pendingSteps = new JSONArray(jsonData);
|
||||
|
||||
// Cycle through so that each Step can be individually processed
|
||||
Path relativePath = antJsonFile.toPath().getParent();
|
||||
for(int i = 0; i < pendingSteps.length(); i++) {
|
||||
JSONObject jsonStep = pendingSteps.getJSONObject(i);
|
||||
System.out.println();
|
||||
try {
|
||||
ingestStep(jsonStep, relativePath);
|
||||
} catch(Exception e) {
|
||||
log.warn("[SKIPPED] Step '{}'", jsonStep, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public JSONArray getJson() {
|
||||
return jsonSteps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct as a Step to perform basic parsing/sanity checks
|
||||
* Copy resources (if needed) to provisioning directory
|
||||
*/
|
||||
private void ingestStep(JSONObject jsonStep, Path relativePath) throws JSONException, IOException {
|
||||
Step step = Step.parse(jsonStep, relativePath);
|
||||
if(!targetOs.matches(step.os)) {
|
||||
log.info("[SKIPPED] Os '{}' does not match target Os '{}' '{}'", Os.serialize(step.os), targetOs, step);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!targetArch.matches(step.arch)) {
|
||||
log.info("[SKIPPED] Arch '{}' does not match target Os '{}' '{}'", Arch.serialize(step.arch), targetArch, step);
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject any special inferences (such as inferring resources from args)
|
||||
inferAdditionalSteps(step);
|
||||
|
||||
if(copyResource(step)) {
|
||||
log.info("[SUCCESS] Step successfully processed '{}'", step);
|
||||
jsonSteps.put(step.toJSON());
|
||||
// Special case for custom websocket ports
|
||||
if(step.getType() == Type.PROPERTY && step.getPhase() == Phase.CERTGEN) {
|
||||
HashMap<String, String> pairs = PropertyInvoker.parsePropertyPairs(step);
|
||||
if(pairs.get(ArgValue.WEBSOCKET_SECURE_PORTS.getMatch()) != null ||
|
||||
pairs.get(ArgValue.WEBSOCKET_INSECURE_PORTS.getMatch()) != null) {
|
||||
// Clone to install step
|
||||
jsonSteps.put(step.cloneTo(Phase.INSTALL).toJSON());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("[SKIPPED] Resources could not be saved '{}'", step);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any resources files required for INSTALL and SCRIPT steps to provision folder
|
||||
*/
|
||||
public boolean copyResource(Step step) throws IOException {
|
||||
switch(step.getType()) {
|
||||
case CA:
|
||||
case CERT:
|
||||
case SCRIPT:
|
||||
case RESOURCE:
|
||||
case SOFTWARE:
|
||||
boolean isRelative = !Paths.get(step.getData()).isAbsolute();
|
||||
File src;
|
||||
if(isRelative) {
|
||||
if(ingestFile != null) {
|
||||
Path parentDir = ingestFile.getParentFile().toPath();
|
||||
src = parentDir.resolve(step.getData()).toFile();
|
||||
} else {
|
||||
throw formatted("Unable to resolve path: '%s' '%s'", step.getData(), step);
|
||||
}
|
||||
} else {
|
||||
src = new File(step.getData());
|
||||
}
|
||||
String fileName = src.getName();
|
||||
if(fileName.equals(BUILD_PROVISION_FILE.getName())) {
|
||||
throw formatted("Resource name conflicts with provision file '%s' '%s'", fileName, step);
|
||||
}
|
||||
File dest = BUILD_PROVISION_FOLDER.resolve(fileName).toFile();
|
||||
int i = 0;
|
||||
// Avoid conflicting file names
|
||||
String name = dest.getName();
|
||||
|
||||
// Avoid resource clobbering when being invoked by command line or providing certificates.
|
||||
// Otherwise, assume the intent is to re-use the same resource (e.g. "my_script.sh", etc)
|
||||
if(ingestFile == null || step.getType() == Type.CERT) {
|
||||
while(dest.exists()) {
|
||||
// Append "filename-1.txt" until there's no longer a conflict
|
||||
if (name.contains(".")) {
|
||||
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%s-%s.%s", FilenameUtils.removeExtension(name), ++i,
|
||||
FilenameUtils.getExtension(name))).toFile();
|
||||
} else {
|
||||
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%-%", name, ++i)).toFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileUtils.copyFile(src, dest);
|
||||
if(dest.exists()) {
|
||||
step.setData(BUILD_PROVISION_FOLDER.relativize(dest.toPath()).toString());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the JSONObject to the end of the provisionFile
|
||||
*/
|
||||
public boolean saveJson(boolean overwrite) throws IOException, JSONException {
|
||||
// Read existing JSON file if exists
|
||||
JSONArray mergeSteps;
|
||||
if(!overwrite && BUILD_PROVISION_FILE.exists()) {
|
||||
String jsonData = FileUtils.readFileToString(BUILD_PROVISION_FILE, StandardCharsets.UTF_8);
|
||||
mergeSteps = new JSONArray(jsonData);
|
||||
} else {
|
||||
mergeSteps = new JSONArray();
|
||||
}
|
||||
|
||||
// Merge in new steps
|
||||
for(int i = 0; i < jsonSteps.length(); i++) {
|
||||
mergeSteps.put(jsonSteps.getJSONObject(i));
|
||||
}
|
||||
|
||||
FileUtils.writeStringToFile(BUILD_PROVISION_FILE, mergeSteps.toString(3), StandardCharsets.UTF_8);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for adding a name/value pair into the JSONObject
|
||||
*/
|
||||
private static void putPattern(JSONObject jsonStep, String name, String val) throws JSONException {
|
||||
if(val != null && !val.isEmpty()) {
|
||||
jsonStep.put(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for adding consecutive patterned value pairs into the JSONObject
|
||||
* e.g. --arg1 "foo" --arg2 "bar"
|
||||
*/
|
||||
private static void putPattern(JSONObject jsonStep, String pattern, String ... varArgs) throws JSONException {
|
||||
int argCounter = 0;
|
||||
for(String arg : varArgs) {
|
||||
jsonStep.put(String.format(pattern, ++argCounter), arg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createProvisionDirectory(boolean cleanDirectory) throws IOException {
|
||||
if(cleanDirectory) {
|
||||
FileUtils.deleteDirectory(BUILD_PROVISION_FOLDER.toFile());
|
||||
}
|
||||
if(BUILD_PROVISION_FOLDER.toFile().isDirectory()) {
|
||||
return;
|
||||
}
|
||||
if(BUILD_PROVISION_FOLDER.toFile().mkdirs()) {
|
||||
return;
|
||||
}
|
||||
throw formatted("Could not create provision destination: '%'", BUILD_PROVISION_FOLDER);
|
||||
}
|
||||
|
||||
private static IOException formatted(String message, Object ... args) {
|
||||
String formatted = String.format(message, args);
|
||||
return new IOException(formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first index of the specified arg prefix pattern(s)
|
||||
*
|
||||
* e.g. if pattern is "/f1", it will return 1 from args { "/s", "/f1C:\foo" }
|
||||
*/
|
||||
private int argPrefixIndex(Step step, String ... prefixes) {
|
||||
for(int i = 0; i < step.args.size() ; i++){
|
||||
for(String prefix : prefixes) {
|
||||
if (step.args.get(i).toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "value" of the specified arg prefix pattern(s)
|
||||
*
|
||||
* e.g. if pattern is "/f1", it will return "C:\foo" from args { "/s", "/f1C:\foo" }
|
||||
*
|
||||
*/
|
||||
private String argPrefixValue(Step step, int index, String ... prefixes) {
|
||||
String arg = step.args.get(index);
|
||||
String value = null;
|
||||
for(String prefix : prefixes) {
|
||||
if (arg.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
value = arg.substring(prefix.length());
|
||||
if((value.startsWith("\"") && value.endsWith("\"")) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))) {
|
||||
// Remove surrounding quotes
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the provided step into a new step that performs a prerequisite task.
|
||||
*
|
||||
* This is "magic" in the sense that it's highly specific to <code>Type</code>
|
||||
* <code>Os</code> and <code>Step.args</code>.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* Older InstallShield installers supported the <code>/f1</code> parameter which
|
||||
* implies an answer file of which we need to bundle for a successful deployment.
|
||||
*/
|
||||
private void inferAdditionalSteps(Step orig) throws JSONException, IOException {
|
||||
// Infer resource step for InstallShield .iss answer files
|
||||
if(orig.getType() == Type.SOFTWARE && Os.WINDOWS.matches(orig.getOs())) {
|
||||
String[] patterns = { "/f1", "-f1" };
|
||||
int index = argPrefixIndex(orig, patterns);
|
||||
if(index > 0) {
|
||||
String resource = argPrefixValue(orig, index, patterns);
|
||||
if(resource != null) {
|
||||
// Clone to copy the Phase, Os and Description
|
||||
Step step = orig.clone();
|
||||
|
||||
// Swap Type, clear args and update the data
|
||||
step.setType(Type.RESOURCE);
|
||||
step.setArgs(new ArrayList<>());
|
||||
step.setData(resource);
|
||||
|
||||
if(copyResource(step)) {
|
||||
File resourceFile = new File(resource);
|
||||
jsonSteps.put(step.toJSON());
|
||||
orig.getArgs().set(index, String.format("/f1\"%s\"", resourceFile.getName()));
|
||||
log.info("[SUCCESS] Step successfully inferred and appended '{}'", step);
|
||||
} else {
|
||||
log.error("[SKIPPED] Resources could not be saved '{}'", step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
337
old code/tray/src/qz/build/provision/Step.java
Executable file
337
old code/tray/src/qz/build/provision/Step.java
Executable file
@@ -0,0 +1,337 @@
|
||||
package qz.build.provision;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.build.provision.params.Arch;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.build.provision.params.Type;
|
||||
import qz.build.provision.params.types.Remover;
|
||||
import qz.build.provision.params.types.Software;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class Step {
|
||||
protected static final Logger log = LogManager.getLogger(Step.class);
|
||||
|
||||
String description;
|
||||
Type type;
|
||||
List<String> args; // Type.SCRIPT or Type.INSTALLER or Type.CONF only
|
||||
HashSet<Os> os;
|
||||
HashSet<Arch> arch;
|
||||
Phase phase;
|
||||
String data;
|
||||
|
||||
Path relativePath;
|
||||
Class relativeClass;
|
||||
|
||||
public Step(Path relativePath, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
|
||||
this.relativePath = relativePath;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.os = os;
|
||||
this.arch = arch;
|
||||
this.phase = phase;
|
||||
this.data = data;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only should be used by unit tests
|
||||
*/
|
||||
Step(Class relativeClass, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
|
||||
this.relativeClass = relativeClass;
|
||||
this.description = description;
|
||||
this.type = type;
|
||||
this.os = os;
|
||||
this.arch = arch;
|
||||
this.phase = phase;
|
||||
this.data = data;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Step { " +
|
||||
"description=\"" + description + "\", " +
|
||||
"type=\"" + type + "\", " +
|
||||
"os=\"" + Os.serialize(os) + "\", " +
|
||||
"arch=\"" + Arch.serialize(arch) + "\", " +
|
||||
"phase=\"" + phase + "\", " +
|
||||
"data=\"" + data + "\", " +
|
||||
"args=\"" + StringUtils.join(args, ",") + "\" " +
|
||||
"}";
|
||||
}
|
||||
|
||||
public JSONObject toJSON() throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("description", description)
|
||||
.put("type", type)
|
||||
.put("os", Os.serialize(os))
|
||||
.put("arch", Arch.serialize(arch))
|
||||
.put("phase", phase)
|
||||
.put("data", data);
|
||||
|
||||
for(int i = 0; i < args.size(); i++) {
|
||||
json.put(String.format("arg%s", i + 1), args.get(i));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public List<String> getArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
public void setArgs(List<String> args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public HashSet<Os> getOs() {
|
||||
return os;
|
||||
}
|
||||
|
||||
public void setOs(HashSet<Os> os) {
|
||||
this.os = os;
|
||||
}
|
||||
|
||||
public HashSet<Arch> getArch() {
|
||||
return arch;
|
||||
}
|
||||
|
||||
public void setArch(HashSet<Arch> arch) {
|
||||
this.arch = arch;
|
||||
}
|
||||
|
||||
public Phase getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(Phase phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Class getRelativeClass() {
|
||||
return relativeClass;
|
||||
}
|
||||
|
||||
public Path getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public boolean usingClass() {
|
||||
return relativeClass != null;
|
||||
}
|
||||
|
||||
public boolean usingPath() {
|
||||
return relativePath != null;
|
||||
}
|
||||
|
||||
public static Step parse(JSONObject jsonStep, Object relativeObject) {
|
||||
String description = jsonStep.optString("description", "");
|
||||
Type type = Type.parse(jsonStep.optString("type", null));
|
||||
String data = jsonStep.optString("data", null);
|
||||
|
||||
// Handle installer args
|
||||
List<String> args = new LinkedList<>();
|
||||
if(type == Type.SOFTWARE || type == Type.CONF) {
|
||||
// Handle space-delimited args
|
||||
args = Software.parseArgs(jsonStep.optString("args", ""));
|
||||
// Handle standalone single args (won't break on whitespace)
|
||||
// e.g. "arg1": "C:\Program Files\Foo"
|
||||
int argCounter = 0;
|
||||
while(true) {
|
||||
String singleArg = jsonStep.optString(String.format("arg%d", ++argCounter), "");
|
||||
if(!singleArg.trim().isEmpty()) {
|
||||
args.add(singleArg.trim());
|
||||
} else {
|
||||
// stop searching if the next incremental arg (e.g. "arg2") isn't found
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mandate "args" as the CONF path
|
||||
if(type == Type.CONF) {
|
||||
// Honor "path" first, if provided
|
||||
String path = jsonStep.optString("path", "");
|
||||
if(!path.isEmpty()) {
|
||||
args.add(0, path);
|
||||
}
|
||||
|
||||
// Keep only the first value
|
||||
if(args.size() > 0) {
|
||||
args = args.subList(0, 1);
|
||||
} else {
|
||||
throw formatted("Conf path value cannot be blank.");
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Os> os = new HashSet<>();
|
||||
if(jsonStep.has("os")) {
|
||||
// Do not tolerate bad os values
|
||||
String osString = jsonStep.optString("os");
|
||||
os = Os.parse(osString);
|
||||
if(os.size() == 0) {
|
||||
throw formatted("Os provided '%s' could not be parsed", osString);
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<Arch> arch = new HashSet<>();
|
||||
if(jsonStep.has("arch")) {
|
||||
// Do not tolerate bad arch values
|
||||
String archString = jsonStep.optString("arch");
|
||||
arch = Arch.parse(archString);
|
||||
if(arch.size() == 0) {
|
||||
throw formatted("Arch provided \"%s\" could not be parsed", archString);
|
||||
}
|
||||
}
|
||||
|
||||
Phase phase = null;
|
||||
if(jsonStep.has("phase")) {
|
||||
String phaseString = jsonStep.optString("phase", null);
|
||||
phase = Phase.parse(phaseString);
|
||||
if(phase == null) {
|
||||
log.warn("Phase provided \"{}\" could not be parsed", phaseString);
|
||||
}
|
||||
}
|
||||
Step step;
|
||||
if(relativeObject instanceof Path) {
|
||||
step = new Step((Path)relativeObject, description, type, os, arch, phase, data, args);
|
||||
} else if(relativeObject instanceof Class) {
|
||||
step = new Step((Class)relativeObject, description, type, os, arch, phase, data, args);
|
||||
} else {
|
||||
throw formatted("Parameter relativeObject must be of type 'Path' or 'Class' but '%s' was provided", relativeObject.getClass());
|
||||
}
|
||||
return step.sanitize();
|
||||
}
|
||||
|
||||
private Step sanitize() {
|
||||
return throwIfNull("Type", type)
|
||||
.throwIfNull("Data", data)
|
||||
.validateOs()
|
||||
.validateArch()
|
||||
.enforcePhase(Type.PREFERENCE, Phase.STARTUP)
|
||||
.enforcePhase(Type.CA, Phase.CERTGEN)
|
||||
.enforcePhase(Type.CERT, Phase.STARTUP)
|
||||
.enforcePhase(Type.CONF, Phase.CERTGEN)
|
||||
.enforcePhase(Type.SOFTWARE, Phase.INSTALL)
|
||||
.enforcePhase(Type.REMOVER, Phase.INSTALL)
|
||||
.enforcePhase(Type.PROPERTY, Phase.CERTGEN, Phase.INSTALL)
|
||||
.validateRemover();
|
||||
}
|
||||
|
||||
private Step validateRemover() {
|
||||
if(type != Type.REMOVER) {
|
||||
return this;
|
||||
}
|
||||
Remover remover = Remover.parse(data);
|
||||
switch(remover) {
|
||||
case CUSTOM:
|
||||
break;
|
||||
default:
|
||||
if(remover.matchesCurrentSystem()) {
|
||||
throw formatted("Remover '%s' would conflict with this installer, skipping. ", remover);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Custom removers must have three elements
|
||||
if(data == null || data.split(",").length != 3) {
|
||||
throw formatted("Remover data '%s' is invalid. Data must match a known type [%s] or contain exactly 3 elements.", data, Remover.valuesDelimited(","));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Step throwIfNull(String name, Object value) {
|
||||
if(value == null) {
|
||||
throw formatted("%s cannot be null", name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Step validateOs() {
|
||||
if(os == null) {
|
||||
if(type == Type.SOFTWARE) {
|
||||
// Software must default to a sane operating system
|
||||
os = Software.parse(data).defaultOs();
|
||||
} else {
|
||||
os = new HashSet<>();
|
||||
}
|
||||
}
|
||||
if(os.size() == 0) {
|
||||
os.add(Os.ALL);
|
||||
log.debug("Os list is null, assuming '{}'", Os.ALL);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Step validateArch() {
|
||||
if(arch == null) {
|
||||
arch = new HashSet<>();
|
||||
}
|
||||
if(arch.size() == 0) {
|
||||
arch.add(Arch.ALL);
|
||||
log.debug("Arch list is null, assuming '{}'", Arch.ALL);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Step enforcePhase(Type matchType, Phase ... requiredPhases) {
|
||||
if(requiredPhases.length == 0) {
|
||||
throw new UnsupportedOperationException("At least one Phase must be specified");
|
||||
}
|
||||
if(type == matchType) {
|
||||
for(Phase requiredPhase : requiredPhases) {
|
||||
if (phase == null) {
|
||||
phase = requiredPhase;
|
||||
log.debug("Phase is null, defaulting to '{}' based on Type '{}'", phase, type);
|
||||
return this;
|
||||
} else if (phase == requiredPhase) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
log.debug("Phase '{}' is unsupported for Type '{}', defaulting to '{}'", phase, type, phase = requiredPhases[0]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private static UnsupportedOperationException formatted(String message, Object ... args) {
|
||||
String formatted = String.format(message, args);
|
||||
return new UnsupportedOperationException(formatted);
|
||||
}
|
||||
Step cloneTo(Phase phase) {
|
||||
return relativePath != null ?
|
||||
new Step(relativePath, description, type, os, arch, phase, data, args) :
|
||||
new Step(relativeClass, description, type, os, arch, phase, data, args);
|
||||
}
|
||||
|
||||
public Step clone() {
|
||||
return cloneTo(this.phase);
|
||||
}
|
||||
}
|
||||
70
old code/tray/src/qz/build/provision/params/Arch.java
Executable file
70
old code/tray/src/qz/build/provision/params/Arch.java
Executable file
@@ -0,0 +1,70 @@
|
||||
package qz.build.provision.params;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Basic architecture parser
|
||||
*
|
||||
* Note: All aliases must be lowercase
|
||||
*/
|
||||
public enum Arch {
|
||||
X86("x32", "i386", "i486", "i586", "i686"),
|
||||
X86_64("amd64"),
|
||||
ARM32("arm", "armv1", "armv2", "armv3", "armv4", "armv5", "armv6", "armv7"),
|
||||
AARCH64("arm64", "armv8", "armv9"),
|
||||
RISCV32("rv32"),
|
||||
RISCV64("rv64"),
|
||||
PPC64("powerpc", "powerpc64"),
|
||||
ALL(), // special handling
|
||||
UNKNOWN();
|
||||
|
||||
private HashSet<String> aliases = new HashSet<>();
|
||||
Arch(String ... aliases) {
|
||||
this.aliases.add(name().toLowerCase(Locale.ENGLISH));
|
||||
this.aliases.addAll(Arrays.asList(aliases));
|
||||
}
|
||||
|
||||
public static Arch parseStrict(String input) throws UnsupportedOperationException {
|
||||
return EnumParser.parseStrict(Arch.class, input, ALL, UNKNOWN);
|
||||
}
|
||||
|
||||
public static HashSet<Arch> parse(String input) {
|
||||
return EnumParser.parseSet(Arch.class, Arch.ALL, input);
|
||||
}
|
||||
|
||||
public static Arch parse(String input, Arch fallback) {
|
||||
Arch found = bestMatch(input);
|
||||
return found == UNKNOWN ? fallback : found;
|
||||
}
|
||||
|
||||
public static Arch bestMatch(String input) {
|
||||
if(input != null) {
|
||||
for(Arch arch : values()) {
|
||||
if (arch.aliases.contains(input.toLowerCase())) {
|
||||
return arch;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Arch.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean matches(HashSet<Arch> archList) {
|
||||
return this == ALL || archList.contains(ALL) || (this != UNKNOWN && archList.contains(this));
|
||||
}
|
||||
|
||||
public static String serialize(HashSet<Arch> archList) {
|
||||
if(archList.contains(ALL)) {
|
||||
return "*";
|
||||
}
|
||||
return StringUtils.join(archList, "|");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
64
old code/tray/src/qz/build/provision/params/EnumParser.java
Executable file
64
old code/tray/src/qz/build/provision/params/EnumParser.java
Executable file
@@ -0,0 +1,64 @@
|
||||
package qz.build.provision.params;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
|
||||
public interface EnumParser {
|
||||
/**
|
||||
* Basic enum parser
|
||||
*/
|
||||
|
||||
static <T extends Enum<T>> T parse(Class<T> clazz, String s) {
|
||||
return parse(clazz, s, null);
|
||||
}
|
||||
|
||||
static <T extends Enum<T>> T parse(Class<T> clazz, String s, T fallbackValue) {
|
||||
if(s != null) {
|
||||
for(T en : EnumSet.allOf(clazz)) {
|
||||
if (en.name().equalsIgnoreCase(s)) {
|
||||
return en;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
static <T extends Enum<T>> T parseStrict(Class<T> clazz, String s, T ... blocklist) throws UnsupportedOperationException {
|
||||
if(s != null) {
|
||||
HashSet<T> matched = parseSet(clazz, null, s);
|
||||
if (matched.size() == 1) {
|
||||
T returnVal = matched.iterator().next();
|
||||
boolean blocked = false;
|
||||
for(T block : blocklist) {
|
||||
if(returnVal == block) {
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!blocked) {
|
||||
return returnVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new UnsupportedOperationException(String.format("%s value '%s' failed to match one and only one item", clazz.getSimpleName(), s));
|
||||
}
|
||||
|
||||
static <T extends Enum<T>> HashSet<T> parseSet(Class<T> clazz, T all, String s) {
|
||||
HashSet<T> matched = new HashSet<>();
|
||||
if(s != null) {
|
||||
// Handle ALL="*"
|
||||
if (all != null && s.equals("*")) {
|
||||
matched.add(all);
|
||||
}
|
||||
|
||||
String[] parts = s.split("\\|");
|
||||
for(String part : parts) {
|
||||
T parsed = parse(clazz, part);
|
||||
if (parsed != null) {
|
||||
matched.add(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
67
old code/tray/src/qz/build/provision/params/Os.java
Executable file
67
old code/tray/src/qz/build/provision/params/Os.java
Executable file
@@ -0,0 +1,67 @@
|
||||
package qz.build.provision.params;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Basic OS parser
|
||||
*/
|
||||
public enum Os {
|
||||
WINDOWS,
|
||||
MAC,
|
||||
LINUX,
|
||||
SOLARIS, // unsupported
|
||||
ALL, // special handling
|
||||
UNKNOWN;
|
||||
|
||||
public boolean matches(HashSet<Os> osList) {
|
||||
return this == ALL || osList.contains(ALL) || (this != UNKNOWN && osList.contains(this));
|
||||
}
|
||||
|
||||
public static boolean matchesHost(HashSet<Os> osList) {
|
||||
for(Os os : osList) {
|
||||
if(os == SystemUtilities.getOs() || os == ALL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Os parseStrict(String input) throws UnsupportedOperationException {
|
||||
return EnumParser.parseStrict(Os.class, input, ALL, UNKNOWN);
|
||||
}
|
||||
|
||||
public static Os bestMatch(String input) {
|
||||
if(input != null) {
|
||||
String name = input.toLowerCase(Locale.ENGLISH);
|
||||
if (name.contains("win")) {
|
||||
return Os.WINDOWS;
|
||||
} else if (name.contains("mac")) {
|
||||
return Os.MAC;
|
||||
} else if (name.contains("linux")) {
|
||||
return Os.LINUX;
|
||||
} else if (name.contains("sunos")) {
|
||||
return Os.SOLARIS;
|
||||
}
|
||||
}
|
||||
return Os.UNKNOWN;
|
||||
}
|
||||
|
||||
public static HashSet<Os> parse(String input) {
|
||||
return EnumParser.parseSet(Os.class, Os.ALL, input);
|
||||
}
|
||||
|
||||
public static String serialize(HashSet<Os> osList) {
|
||||
if(osList.contains(ALL)) {
|
||||
return "*";
|
||||
}
|
||||
return StringUtils.join(osList, "|");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
19
old code/tray/src/qz/build/provision/params/Phase.java
Executable file
19
old code/tray/src/qz/build/provision/params/Phase.java
Executable file
@@ -0,0 +1,19 @@
|
||||
package qz.build.provision.params;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Phase {
|
||||
INSTALL,
|
||||
CERTGEN,
|
||||
STARTUP,
|
||||
UNINSTALL;
|
||||
|
||||
public static Phase parse(String input) {
|
||||
return EnumParser.parse(Phase.class, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
24
old code/tray/src/qz/build/provision/params/Type.java
Executable file
24
old code/tray/src/qz/build/provision/params/Type.java
Executable file
@@ -0,0 +1,24 @@
|
||||
package qz.build.provision.params;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Type {
|
||||
SCRIPT,
|
||||
SOFTWARE,
|
||||
RESOURCE,
|
||||
REMOVER, // QZ Tray remover
|
||||
CA,
|
||||
CERT,
|
||||
CONF,
|
||||
PROPERTY,
|
||||
PREFERENCE;
|
||||
|
||||
public static Type parse(String input) {
|
||||
return EnumParser.parse(Type.class, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
67
old code/tray/src/qz/build/provision/params/types/Remover.java
Executable file
67
old code/tray/src/qz/build/provision/params/types/Remover.java
Executable file
@@ -0,0 +1,67 @@
|
||||
package qz.build.provision.params.types;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import qz.build.provision.params.EnumParser;
|
||||
import qz.common.Constants;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Remover {
|
||||
QZ("QZ Tray", "qz-tray", "qz"),
|
||||
CUSTOM(null, null, null); // reserved
|
||||
|
||||
private String aboutTitle;
|
||||
private String propsFile;
|
||||
private String dataDir;
|
||||
|
||||
Remover(String aboutTitle, String propsFile, String dataDir) {
|
||||
this.aboutTitle = aboutTitle;
|
||||
this.propsFile = propsFile;
|
||||
this.dataDir = dataDir;
|
||||
}
|
||||
|
||||
public String getAboutTitle() {
|
||||
return aboutTitle;
|
||||
}
|
||||
|
||||
public String getPropsFile() {
|
||||
return propsFile;
|
||||
}
|
||||
|
||||
public String getDataDir() {
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
public static String valuesDelimited(String delimiter) {
|
||||
ArrayList<Remover> listing = new ArrayList<>(Arrays.asList(values()));
|
||||
listing.remove(CUSTOM);
|
||||
return StringUtils.join(listing, delimiter).toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to custom if not found
|
||||
*/
|
||||
public static Remover parse(String input) {
|
||||
Remover remover = EnumParser.parse(Remover.class, input);
|
||||
if(remover == CUSTOM) {
|
||||
throw new UnsupportedOperationException("Remover 'custom' is reserved for internal purposes");
|
||||
}
|
||||
if(remover == null) {
|
||||
remover = CUSTOM;
|
||||
}
|
||||
return remover;
|
||||
}
|
||||
|
||||
public boolean matchesCurrentSystem() {
|
||||
return Constants.ABOUT_TITLE.equals(aboutTitle) ||
|
||||
Constants.PROPS_FILE.equals(propsFile) ||
|
||||
Constants.DATA_DIR.equals(dataDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
31
old code/tray/src/qz/build/provision/params/types/Script.java
Executable file
31
old code/tray/src/qz/build/provision/params/types/Script.java
Executable file
@@ -0,0 +1,31 @@
|
||||
package qz.build.provision.params.types;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import qz.build.provision.params.EnumParser;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Script {
|
||||
PS1,
|
||||
BAT,
|
||||
SH,
|
||||
PY,
|
||||
RB;
|
||||
|
||||
public static Script parse(String input) {
|
||||
if(input != null && !input.isEmpty()) {
|
||||
return EnumParser.parse(Script.class, FilenameUtils.getExtension(input), SH);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Script parse(Path path) {
|
||||
return parse(path.toAbsolutePath().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
63
old code/tray/src/qz/build/provision/params/types/Software.java
Executable file
63
old code/tray/src/qz/build/provision/params/types/Software.java
Executable file
@@ -0,0 +1,63 @@
|
||||
package qz.build.provision.params.types;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import qz.build.provision.params.EnumParser;
|
||||
import qz.build.provision.params.Os;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public enum Software {
|
||||
EXE,
|
||||
MSI,
|
||||
PKG,
|
||||
DMG,
|
||||
RUN,
|
||||
UNKNOWN;
|
||||
|
||||
public static Software parse(String input) {
|
||||
return EnumParser.parse(Software.class, FilenameUtils.getExtension(input), UNKNOWN);
|
||||
}
|
||||
|
||||
public static Software parse(Path path) {
|
||||
return parse(path.toString());
|
||||
}
|
||||
|
||||
public static List<String> parseArgs(String input) {
|
||||
List<String> args = new LinkedList<>();
|
||||
if(input != null) {
|
||||
String[] parts = input.split(" ");
|
||||
for(String part : parts) {
|
||||
if(!part.trim().isEmpty()) {
|
||||
args.add(part.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
public HashSet<Os> defaultOs() {
|
||||
HashSet<Os> list = new HashSet<>();
|
||||
switch(this) {
|
||||
case EXE:
|
||||
case MSI:
|
||||
list.add(Os.WINDOWS);
|
||||
break;
|
||||
case PKG:
|
||||
case DMG:
|
||||
list.add(Os.MAC);
|
||||
break;
|
||||
case RUN:
|
||||
list.add(Os.LINUX);
|
||||
break;
|
||||
default:
|
||||
list.add(Os.ALL);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
205
old code/tray/src/qz/common/AboutInfo.java
Executable file
205
old code/tray/src/qz/common/AboutInfo.java
Executable file
@@ -0,0 +1,205 @@
|
||||
package qz.common;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils;
|
||||
import org.apache.commons.ssl.Base64;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.x509.extension.X509ExtensionUtil;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.certificate.KeyPairWrapper;
|
||||
import qz.installer.certificate.CertificateManager;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.StringUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.PrintSocketServer;
|
||||
import qz.ws.WebsocketPorts;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
public class AboutInfo {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(AboutInfo.class);
|
||||
|
||||
private static String preferredHostname = "localhost";
|
||||
|
||||
public static JSONObject gatherAbout(String domain, CertificateManager certificateManager) {
|
||||
JSONObject about = new JSONObject();
|
||||
|
||||
try {
|
||||
about.put("product", product());
|
||||
about.put("socket", socket(certificateManager, domain));
|
||||
about.put("environment", environment());
|
||||
about.put("ssl", ssl(certificateManager));
|
||||
about.put("libraries", libraries());
|
||||
about.put("charsets", charsets());
|
||||
}
|
||||
catch(JSONException | GeneralSecurityException e) {
|
||||
log.error("Failed to write JSON data", e);
|
||||
}
|
||||
|
||||
return about;
|
||||
}
|
||||
|
||||
private static JSONObject product() throws JSONException {
|
||||
JSONObject product = new JSONObject();
|
||||
|
||||
product
|
||||
.put("title", Constants.ABOUT_TITLE)
|
||||
.put("version", Constants.VERSION)
|
||||
.put("vendor", Constants.ABOUT_COMPANY)
|
||||
.put("url", Constants.ABOUT_URL);
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
private static JSONObject socket(CertificateManager certificateManager, String domain) throws JSONException {
|
||||
JSONObject socket = new JSONObject();
|
||||
String sanitizeDomain = StringUtilities.escapeHtmlEntities(domain);
|
||||
WebsocketPorts websocketPorts = PrintSocketServer.getWebsocketPorts();
|
||||
|
||||
// Gracefully handle XSS per https://github.com/qzind/tray/issues/1099
|
||||
if(sanitizeDomain.contains("<") || sanitizeDomain.contains(">")) {
|
||||
log.warn("Something smells fishy about this domain: \"{}\", skipping", domain);
|
||||
sanitizeDomain = "unknown";
|
||||
}
|
||||
|
||||
socket
|
||||
.put("domain", sanitizeDomain)
|
||||
.put("secureProtocol", "wss")
|
||||
.put("securePort", certificateManager.isSslActive() ? websocketPorts.getSecurePort() : "none")
|
||||
.put("insecureProtocol", "ws")
|
||||
.put("insecurePort", websocketPorts.getInsecurePort());
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private static JSONObject environment() throws JSONException {
|
||||
JSONObject environment = new JSONObject();
|
||||
|
||||
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
|
||||
|
||||
environment
|
||||
.put("os", SystemUtilities.getOsDisplayName())
|
||||
.put("os version", SystemUtilities.getOsDisplayVersion())
|
||||
.put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getArch().toString().toLowerCase()))
|
||||
.put("java (location)", System.getProperty("java.home"))
|
||||
.put("java (vendor)", Constants.JAVA_VENDOR)
|
||||
.put("uptime", DurationFormatUtils.formatDurationWords(uptime, true, false))
|
||||
.put("uptimeMillis", uptime)
|
||||
.put("sandbox", SystemUtilities.isMac() && MacUtilities.isSandboxed());
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
private static JSONObject ssl(CertificateManager certificateManager) throws JSONException, CertificateEncodingException {
|
||||
JSONObject ssl = new JSONObject();
|
||||
|
||||
JSONArray certs = new JSONArray();
|
||||
|
||||
for (KeyPairWrapper keyPair : new KeyPairWrapper[]{certificateManager.getCaKeyPair(), certificateManager.getSslKeyPair() }) {
|
||||
X509Certificate x509 = keyPair.getCert();
|
||||
if (x509 != null) {
|
||||
JSONObject cert = new JSONObject();
|
||||
cert.put("alias", keyPair.getAlias());
|
||||
try {
|
||||
ASN1Primitive ext = X509ExtensionUtil.fromExtensionValue(x509.getExtensionValue(Extension.basicConstraints.getId()));
|
||||
cert.put("rootca", BasicConstraints.getInstance(ext).isCA());
|
||||
}
|
||||
catch(IOException | NullPointerException e) {
|
||||
cert.put("rootca", false);
|
||||
}
|
||||
cert.put("subject", x509.getSubjectX500Principal().getName());
|
||||
cert.put("expires", SystemUtilities.toISO(x509.getNotAfter()));
|
||||
cert.put("data", formatCert(x509.getEncoded()));
|
||||
certs.put(cert);
|
||||
}
|
||||
}
|
||||
ssl.put("certificates", certs);
|
||||
|
||||
return ssl;
|
||||
}
|
||||
|
||||
public static String formatCert(byte[] encoding) {
|
||||
return "-----BEGIN CERTIFICATE-----\r\n" +
|
||||
new String(Base64.encodeBase64(encoding, true), StandardCharsets.UTF_8) +
|
||||
"-----END CERTIFICATE-----\r\n";
|
||||
}
|
||||
|
||||
private static JSONObject libraries() throws JSONException {
|
||||
JSONObject libraries = new JSONObject();
|
||||
|
||||
SortedMap<String,String> libs = SecurityInfo.getLibVersions();
|
||||
for(Map.Entry<String,String> entry : libs.entrySet()) {
|
||||
String version = entry.getValue();
|
||||
if (version == null) { version = "unknown"; }
|
||||
|
||||
libraries.put(entry.getKey(), version);
|
||||
}
|
||||
|
||||
return libraries;
|
||||
}
|
||||
|
||||
private static JSONObject charsets() throws JSONException {
|
||||
JSONObject charsets = new JSONObject();
|
||||
|
||||
SortedMap<String,Charset> avail = Charset.availableCharsets();
|
||||
ArrayList<String> names = new ArrayList<>();
|
||||
for(Map.Entry<String,Charset> entry : avail.entrySet()) {
|
||||
names.add(entry.getValue().name());
|
||||
}
|
||||
|
||||
charsets.put("charsets", Arrays.toString(names.toArray()));
|
||||
return charsets;
|
||||
}
|
||||
|
||||
public static String getPreferredHostname() {
|
||||
return preferredHostname;
|
||||
}
|
||||
|
||||
public static Version findLatestVersion() {
|
||||
log.trace("Looking for newer versions of {} online", Constants.ABOUT_TITLE);
|
||||
try {
|
||||
URL api = new URL(Constants.VERSION_CHECK_URL);
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(api.openStream()));
|
||||
|
||||
StringBuilder rawJson = new StringBuilder();
|
||||
String line;
|
||||
while((line = br.readLine()) != null) {
|
||||
rawJson.append(line);
|
||||
}
|
||||
|
||||
JSONArray versions = new JSONArray(rawJson.toString());
|
||||
for(int i = 0; i < versions.length(); i++) {
|
||||
JSONObject versionData = versions.getJSONObject(i);
|
||||
if(versionData.getString("target_commitish").equals("master")) {
|
||||
Version latestVersion = Version.valueOf(versionData.getString("name"));
|
||||
log.trace("Found latest version of {} online: {}", Constants.ABOUT_TITLE, latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
}
|
||||
throw new Exception("Could not find valid json version information online.");
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Failed to get latest version of {} online", Constants.ABOUT_TITLE, e);
|
||||
}
|
||||
|
||||
return Constants.VERSION;
|
||||
}
|
||||
|
||||
}
|
||||
156
old code/tray/src/qz/common/ByteArrayBuilder.java
Executable file
156
old code/tray/src/qz/common/ByteArrayBuilder.java
Executable file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @author Antoni Ten Monro's
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
* Copyright (C) 2013 Antoni Ten Monro's
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*
|
||||
*/
|
||||
|
||||
package qz.common;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a simple and efficient way for concatenating byte arrays, similar
|
||||
* in purpose to <code>StringBuilder</code>. Objects of this class are not
|
||||
* thread safe and include no synchronization
|
||||
*
|
||||
* @author Antoni Ten Monro's
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public final class ByteArrayBuilder {
|
||||
|
||||
private List<Byte> buffer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to 10
|
||||
*/
|
||||
public ByteArrayBuilder() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to
|
||||
* <code>initialCapacity</code>
|
||||
*
|
||||
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public ByteArrayBuilder(int initialCapacity) {
|
||||
this(null, initialCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to 10
|
||||
* and appends <code>initialContents</code>
|
||||
*
|
||||
* @param initialContents the initial contents of the ByteArrayBuilder
|
||||
*/
|
||||
public ByteArrayBuilder(byte[] initialContents) {
|
||||
this(initialContents, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to
|
||||
* <code>initialContents</code> and appends <code>initialContents</code>
|
||||
*
|
||||
* @param initialContents the initial contents of the <code>ByteArrayBuilder</code>
|
||||
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public ByteArrayBuilder(byte[] initialContents, int initialCapacity) {
|
||||
buffer = new ArrayList<>(initialCapacity);
|
||||
if (initialContents != null) {
|
||||
append(initialContents);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public void clear() {
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a portion of the <code>ByteArrayBuilder</code>
|
||||
*
|
||||
* @param startIndex Starting index, inclusive
|
||||
* @param endIndex Ending index, exclusive
|
||||
*/
|
||||
public final void clearRange(int startIndex, int endIndex) {
|
||||
buffer.subList(startIndex, endIndex).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the number of bytes currently stored in this <code>ByteArrayBuilder</code>
|
||||
*
|
||||
* @return the number of bytes in the <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public int getLength() {
|
||||
return buffer.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a new byte array to this <code>ByteArrayBuilder</code>.
|
||||
* Returns this same object to allow chaining calls
|
||||
*
|
||||
* @param bytes the byte array to append
|
||||
* @return this <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public final ByteArrayBuilder append(byte[] bytes) {
|
||||
for(byte b : bytes) {
|
||||
buffer.add(b);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public final ByteArrayBuilder append(List<Byte> bytes) {
|
||||
for(byte b : bytes) {
|
||||
buffer.add(b);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for append(byte[]) combined with a StringBuffer of specified
|
||||
* charset
|
||||
*
|
||||
* @param string the String to append
|
||||
* @param charset the Charset of the String
|
||||
* @return this <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public final ByteArrayBuilder append(String string, Charset charset) throws UnsupportedEncodingException {
|
||||
return append(string.getBytes(charset.name()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for append(byte[]) combined with a String of specified
|
||||
* charset
|
||||
*
|
||||
* @param stringBuilder the StringBuilder to append
|
||||
* @param charset the Charset of the StringBuilder
|
||||
* @return this <code>ByteArrayBuilder</code>
|
||||
*/
|
||||
public final ByteArrayBuilder append(StringBuilder stringBuilder, Charset charset) throws UnsupportedEncodingException {
|
||||
return append(stringBuilder.toString(), charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full contents of this <code>ByteArrayBuilder</code> as
|
||||
* a single <code>byte</code> array.
|
||||
*
|
||||
* @return The contents of this <code>ByteArrayBuilder</code> as a single <code>byte</code> array
|
||||
*/
|
||||
public byte[] getByteArray() {
|
||||
return ArrayUtils.toPrimitive(buffer.toArray(new Byte[buffer.size()]));
|
||||
}
|
||||
}
|
||||
97
old code/tray/src/qz/common/CachedObject.java
Executable file
97
old code/tray/src/qz/common/CachedObject.java
Executable file
@@ -0,0 +1,97 @@
|
||||
package qz.common;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A generic class that encapsulates an object for caching. The cached object
|
||||
* will be refreshed automatically when accessed after its lifespan has expired.
|
||||
*
|
||||
* @param <T> The type of object to be cached.
|
||||
*/
|
||||
public class CachedObject<T> {
|
||||
public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds
|
||||
T lastObject;
|
||||
Supplier<T> supplier;
|
||||
private long timestamp;
|
||||
private long lifespan;
|
||||
|
||||
/**
|
||||
* Creates a new CachedObject with a default lifespan of 5000 milliseconds
|
||||
*
|
||||
* @param supplier The function to pull new values from
|
||||
*/
|
||||
public CachedObject(Supplier<T> supplier) {
|
||||
this(supplier, DEFAULT_LIFESPAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new CachedObject
|
||||
*
|
||||
* @param supplier The function to pull new values from
|
||||
* @param lifespan The lifespan of the cached object in milliseconds
|
||||
*/
|
||||
public CachedObject(Supplier<T> supplier, long lifespan) {
|
||||
this.supplier = supplier;
|
||||
setLifespan(lifespan);
|
||||
timestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, MIN_VALUE guarantees a first-run.
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new supplier for the CachedObject
|
||||
*
|
||||
* @param supplier The function to pull new values from
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public void registerSupplier(Supplier<T> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lifespan of the cached object
|
||||
*
|
||||
* @param milliseconds The lifespan of the cached object in milliseconds
|
||||
*/
|
||||
public void setLifespan(long milliseconds) {
|
||||
lifespan = Math.max(0, milliseconds); // prevent overflow
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cached object.
|
||||
* If the cached object's lifespan has expired, it gets refreshed before being returned.
|
||||
*
|
||||
* @return The cached object
|
||||
*/
|
||||
public T get() {
|
||||
return get(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cached object.
|
||||
* If the cached object's lifespan is expired or forceRefresh is true, it gets refreshed before being returned.
|
||||
*
|
||||
* @param forceRefresh If true, the cached object will be refreshed before being returned regardless of its lifespan
|
||||
* @return The cached object
|
||||
*/
|
||||
public T get(boolean forceRefresh) {
|
||||
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
|
||||
// check lifespan
|
||||
if (forceRefresh || (timestamp + lifespan <= now)) {
|
||||
timestamp = now;
|
||||
lastObject = supplier.get();
|
||||
}
|
||||
return lastObject;
|
||||
}
|
||||
|
||||
// Test
|
||||
public static void main(String ... args) throws InterruptedException {
|
||||
final AtomicInteger testInt = new AtomicInteger(0);
|
||||
|
||||
CachedObject<Integer> cachedString = new CachedObject<>(testInt::incrementAndGet);
|
||||
for(int i = 0; i < 100; i++) {
|
||||
Thread.sleep(1500);
|
||||
System.out.println(cachedString.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
103
old code/tray/src/qz/common/Constants.java
Executable file
103
old code/tray/src/qz/common/Constants.java
Executable file
@@ -0,0 +1,103 @@
|
||||
package qz.common;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
import static qz.ws.SingleInstanceChecker.STEAL_WEBSOCKET_PROPERTY;
|
||||
|
||||
/**
|
||||
* Created by robert on 7/9/2014.
|
||||
*/
|
||||
public class Constants {
|
||||
public static final String HEXES = "0123456789ABCDEF";
|
||||
public static final char[] HEXES_ARRAY = HEXES.toCharArray();
|
||||
public static final int BYTE_BUFFER_SIZE = 8192;
|
||||
public static final Version VERSION = Version.valueOf("2.2.6-SNAPSHOT");
|
||||
public static final Version JAVA_VERSION = SystemUtilities.getJavaVersion();
|
||||
public static final String JAVA_VENDOR = System.getProperty("java.vendor");
|
||||
|
||||
/* QZ-Tray Constants */
|
||||
public static final String BLOCK_FILE = "blocked";
|
||||
public static final String ALLOW_FILE = "allowed";
|
||||
public static final String TEMP_FILE = "temp";
|
||||
public static final String LOG_FILE = "debug";
|
||||
public static final String PROPS_FILE = "qz-tray"; // .properties extension is assumed
|
||||
public static final String PREFS_FILE = "prefs"; // .properties extension is assumed
|
||||
public static final String[] PERSIST_PROPS = {"file.whitelist", "file.allow", "networking.hostname", "networking.port", STEAL_WEBSOCKET_PROPERTY };
|
||||
public static final String AUTOSTART_FILE = ".autostart";
|
||||
public static final String DATA_DIR = "qz";
|
||||
|
||||
public static final int BORDER_PADDING = 10;
|
||||
|
||||
public static final String ABOUT_TITLE = "QZ Tray";
|
||||
public static final String ABOUT_EMAIL = "support@qz.io";
|
||||
public static final String ABOUT_URL = "https://qz.io";
|
||||
public static final String ABOUT_COMPANY = "QZ Industries, LLC";
|
||||
public static final String ABOUT_CITY = "Canastota";
|
||||
public static final String ABOUT_STATE = "NY";
|
||||
public static final String ABOUT_COUNTRY = "US";
|
||||
|
||||
public static final String ABOUT_LICENSING_URL = Constants.ABOUT_URL + "/licensing";
|
||||
public static final String ABOUT_SUPPORT_URL = Constants.ABOUT_URL + "/support";
|
||||
public static final String ABOUT_PRIVACY_URL = Constants.ABOUT_URL + "/privacy";
|
||||
public static final String ABOUT_DOWNLOAD_URL = Constants.ABOUT_URL + "/download";
|
||||
|
||||
public static final String VERSION_CHECK_URL = "https://api.github.com/repos/qzind/tray/releases";
|
||||
public static final String VERSION_DOWNLOAD_URL = "https://github.com/qzind/tray/releases";
|
||||
public static final boolean ENABLE_DIAGNOSTICS = true; // Diagnostics menu (logs, etc)
|
||||
|
||||
public static final String TRUSTED_CERT = String.format("Verified by %s", Constants.ABOUT_COMPANY);
|
||||
public static final String SPONSORED_CERT = String.format("Sponsored by %s", Constants.ABOUT_COMPANY);
|
||||
public static final String SPONSORED_TOOLTIP = "Sponsored organization";
|
||||
public static final String UNTRUSTED_CERT = "Untrusted website";
|
||||
public static final String NO_TRUST = "Cannot verify trust";
|
||||
|
||||
public static final String PROBE_REQUEST = "getProgramName";
|
||||
public static final String PROBE_RESPONSE = ABOUT_TITLE;
|
||||
|
||||
public static final String ALLOW_SITES_TEXT = "Permanently allowed \"%s\" to access local resources";
|
||||
public static final String BLOCK_SITES_TEXT = "Permanently blocked \"%s\" from accessing local resources";
|
||||
|
||||
public static final String REMEMBER_THIS_DECISION = "Remember this decision";
|
||||
public static final String STRICT_MODE_LABEL = "Use strict certificate mode";
|
||||
public static final String STRICT_MODE_TOOLTIP = String.format("Prevents the ability to select \"%s\" for most websites", REMEMBER_THIS_DECISION);
|
||||
public static final String STRICT_MODE_CONFIRM = String.format("Set strict certificate mode? Most websites will stop working with %s.", ABOUT_TITLE);
|
||||
public static final String ALLOW_SITES_LABEL = "Sites permanently allowed access";
|
||||
public static final String BLOCK_SITES_LABEL = "Sites permanently blocked from access";
|
||||
|
||||
|
||||
public static final String ALLOWED = "Allowed";
|
||||
public static final String BLOCKED = "Blocked";
|
||||
|
||||
public static final String OVERRIDE_CERT = "override.crt";
|
||||
public static final String WHITELIST_CERT_DIR = "whitelist";
|
||||
public static final String PROVISION_DIR = "provision";
|
||||
public static final String PROVISION_FILE = "provision.json";
|
||||
|
||||
public static final String SIGNING_PRIVATE_KEY = "private-key.pem";
|
||||
public static final String SIGNING_CERTIFICATE = "digital-certificate.txt";
|
||||
|
||||
public static final long VALID_SIGNING_PERIOD = 15 * 60 * 1000; //millis
|
||||
public static final int EXPIRY_WARN = 30; // days
|
||||
public static final Color WARNING_COLOR_LITE = Color.RED;
|
||||
public static final Color TRUSTED_COLOR_LITE = Color.BLUE;
|
||||
public static final Color WARNING_COLOR_DARK = Color.decode("#EB6261");
|
||||
public static final Color TRUSTED_COLOR_DARK = Color.decode("#589DF6");
|
||||
public static Color WARNING_COLOR = WARNING_COLOR_LITE;
|
||||
public static Color TRUSTED_COLOR = TRUSTED_COLOR_LITE;
|
||||
|
||||
public static boolean MASK_TRAY_SUPPORTED = true;
|
||||
|
||||
public static final long MEMORY_PER_PRINT = 512; //MB
|
||||
|
||||
public static final String RAW_PRINT = ABOUT_TITLE + " Raw Print";
|
||||
public static final String IMAGE_PRINT = ABOUT_TITLE + " Pixel Print";
|
||||
public static final String PDF_PRINT = ABOUT_TITLE + " PDF Print";
|
||||
public static final String HTML_PRINT = ABOUT_TITLE + " HTML Print";
|
||||
|
||||
public static final Integer[] DEFAULT_WSS_PORTS = {8181, 8282, 8383, 8484};
|
||||
public static final Integer[] DEFAULT_WS_PORTS = {8182, 8283, 8384, 8485};
|
||||
public static final Integer[] CUPS_RSS_PORTS = {8586, 8687, 8788, 8889};
|
||||
}
|
||||
100
old code/tray/src/qz/common/PropertyHelper.java
Executable file
100
old code/tray/src/qz/common/PropertyHelper.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.common;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ArgValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
/**
|
||||
* Created by Tres on 12/16/2015.
|
||||
*/
|
||||
public class PropertyHelper extends Properties {
|
||||
private static final Logger log = LogManager.getLogger(PropertyHelper.class);
|
||||
private String file;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public PropertyHelper() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
* @param p Initial Properties
|
||||
*/
|
||||
public PropertyHelper(Properties p) {
|
||||
super(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom constructor, attempts to load from file
|
||||
* @param file File to load properties from
|
||||
*/
|
||||
public PropertyHelper(String file) {
|
||||
super();
|
||||
this.file = file;
|
||||
load(file);
|
||||
}
|
||||
|
||||
public PropertyHelper(File file) {
|
||||
this(file == null ? null : file.getAbsolutePath());
|
||||
}
|
||||
|
||||
public boolean getBoolean(String key, boolean defaultVal) {
|
||||
String prop = getProperty(key);
|
||||
if (prop != null) {
|
||||
return Boolean.parseBoolean(prop);
|
||||
} else {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProperty(ArgValue arg, boolean value) {
|
||||
setProperty(arg.getMatch(), "" + value);
|
||||
}
|
||||
|
||||
public void load(File file) {
|
||||
load(file == null ? null : file.getAbsolutePath());
|
||||
}
|
||||
|
||||
public void load(String file) {
|
||||
FileInputStream f = null;
|
||||
try {
|
||||
f = new FileInputStream(file);
|
||||
load(f);
|
||||
} catch (IOException e) {
|
||||
log.warn("Could not load file: {}, reason: {}", file, e.getLocalizedMessage());
|
||||
} finally {
|
||||
if (f != null) {
|
||||
try { f.close(); } catch(Throwable ignore) {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean save() {
|
||||
boolean success = false;
|
||||
FileOutputStream f = null;
|
||||
try {
|
||||
f = new FileOutputStream(file);
|
||||
this.store(f, null);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
log.error("Error saving file: {}", file, e);
|
||||
} finally {
|
||||
if (f != null) {
|
||||
try { f.close(); } catch(Throwable ignore) {};
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public synchronized Object setProperty(Map.Entry<String, String> pair) {
|
||||
return super.setProperty(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
169
old code/tray/src/qz/common/SecurityInfo.java
Executable file
169
old code/tray/src/qz/common/SecurityInfo.java
Executable file
@@ -0,0 +1,169 @@
|
||||
package qz.common;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.eclipse.jetty.util.Jetty;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.usb4java.LibUsb;
|
||||
import purejavahidapi.PureJavaHidApi;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Created by Kyle B. on 10/27/2017.
|
||||
*/
|
||||
public class SecurityInfo {
|
||||
/**
|
||||
* Wrap throwable operations into a try/catch
|
||||
*/
|
||||
private static class CheckedTreeMap<K, V> extends TreeMap<K, V> {
|
||||
private static final Logger log = LogManager.getLogger(CheckedTreeMap.class);
|
||||
|
||||
interface CheckedValue {
|
||||
Object check() throws Throwable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public V put(K key, CheckedValue value) {
|
||||
try {
|
||||
return put(key, (V)value.check());
|
||||
} catch(Throwable t) {
|
||||
log.warn("A checked exception was suppressed adding key \"{}\"", key, t);
|
||||
return put(key, (V)"missing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SecurityInfo.class);
|
||||
|
||||
public static KeyStore getKeyStore(Properties props) {
|
||||
if (props != null) {
|
||||
String store = props.getProperty("wss.keystore", "");
|
||||
char[] pass = props.getProperty("wss.storepass", "").toCharArray();
|
||||
|
||||
try {
|
||||
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keystore.load(new FileInputStream(store), pass);
|
||||
return keystore;
|
||||
}
|
||||
catch(GeneralSecurityException | IOException e) {
|
||||
log.warn("Unable to create keystore from properties file: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static SortedMap<String,String> getLibVersions() {
|
||||
CheckedTreeMap<String,String> libVersions = new CheckedTreeMap<>();
|
||||
|
||||
// Use API-provided mechanism if available
|
||||
libVersions.put("jna (native)", () -> Native.VERSION_NATIVE);
|
||||
libVersions.put("jna (location)", () -> {
|
||||
@SuppressWarnings("unused")
|
||||
int ignore = Native.BOOL_SIZE;
|
||||
return System.getProperty("jnidispatch.path");
|
||||
});
|
||||
libVersions.put("jna", Native.VERSION);
|
||||
libVersions.put("jssc", () -> jssc.SerialNativeInterface.getLibraryVersion());
|
||||
libVersions.put("jssc (native)", () -> jssc.SerialNativeInterface.getNativeLibraryVersion());
|
||||
libVersions.put("jetty", Jetty.VERSION);
|
||||
libVersions.put("pdfbox", org.apache.pdfbox.util.Version.getVersion());
|
||||
libVersions.put("purejavahidapi", () -> PureJavaHidApi.getVersion());
|
||||
libVersions.put("usb-api", javax.usb.Version.getApiVersion());
|
||||
libVersions.put("not-yet-commons-ssl", org.apache.commons.ssl.Version.VERSION);
|
||||
libVersions.put("mslinks", mslinks.ShellLink.VERSION);
|
||||
libVersions.put("bouncycastle", "" + new BouncyCastleProvider().getVersion());
|
||||
libVersions.put("usb4java (native)", () -> LibUsb.getVersion().toString());
|
||||
|
||||
libVersions.put("jre", Constants.JAVA_VERSION.toString());
|
||||
libVersions.put("jre (vendor)", Constants.JAVA_VENDOR);
|
||||
|
||||
//JFX info, if it exists
|
||||
try {
|
||||
// "DO NOT LINK JAVAFX EVER" - JavaFX may not exist, use reflection to avoid compilation errors
|
||||
Class<?> VersionInfo = Class.forName("com.sun.javafx.runtime.VersionInfo");
|
||||
Path fxPath = Paths.get(VersionInfo.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
Method method = VersionInfo.getMethod("getVersion");
|
||||
Object version = method.invoke(null);
|
||||
libVersions.put("javafx", (String)version);
|
||||
libVersions.put("javafx (location)", fxPath.toString());
|
||||
} catch(Throwable e) {
|
||||
libVersions.put("javafx", "missing");
|
||||
libVersions.put("javafx (location)", "missing");
|
||||
}
|
||||
|
||||
// Fallback to maven manifest information
|
||||
HashMap<String,String> mavenVersions = getMavenVersions();
|
||||
|
||||
String[] mavenLibs = {"jetty-servlet", "jetty-io", "websocket-common",
|
||||
"usb4java-javax", "java-semver", "commons-pool2",
|
||||
"websocket-server", "jettison", "commons-codec", "log4j-api", "log4j-core",
|
||||
"websocket-servlet", "jetty-http", "commons-lang3", "javax-websocket-server-impl",
|
||||
"javax.servlet-api", "hid4java", "usb4java", "websocket-api", "jetty-util", "websocket-client",
|
||||
"javax.websocket-api", "commons-io", "jetty-security"};
|
||||
|
||||
for(String lib : mavenLibs) {
|
||||
libVersions.put(lib, mavenVersions.get(lib));
|
||||
}
|
||||
|
||||
return libVersions;
|
||||
}
|
||||
|
||||
public static void printLibInfo() {
|
||||
String format = "%-40s%s%n";
|
||||
System.out.printf(format, "LIBRARY NAME:", "VERSION:");
|
||||
SortedMap<String,String> libVersions = SecurityInfo.getLibVersions();
|
||||
for(Map.Entry<String,String> entry : libVersions.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
System.out.printf(format, entry.getKey(), "(unknown)");
|
||||
} else {
|
||||
System.out.printf(format, entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches embedded version information based on maven properties
|
||||
*
|
||||
* @return HashMap of library name, version
|
||||
*/
|
||||
private static HashMap<String,String> getMavenVersions() {
|
||||
final HashMap<String,String> mavenVersions = new HashMap<>();
|
||||
String jar = "jar:" + SecurityInfo.class.getProtectionDomain().getCodeSource().getLocation().toString();
|
||||
try(FileSystem fs = FileSystems.newFileSystem(new URI(jar), new HashMap<String,String>())) {
|
||||
Files.walkFileTree(fs.getPath("/META-INF/maven"), new HashSet<>(), 3, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
if (file.toString().endsWith(".properties")) {
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
props.load(Files.newInputStream(file, StandardOpenOption.READ));
|
||||
mavenVersions.put(props.getProperty("artifactId"), props.getProperty("version"));
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.warn("Error reading properties from {}", file, e);
|
||||
}
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Exception ignore) {
|
||||
log.warn("Could not open {} for version information. Most libraries will list as (unknown)", jar);
|
||||
}
|
||||
return mavenVersions;
|
||||
}
|
||||
|
||||
}
|
||||
693
old code/tray/src/qz/common/TrayManager.java
Executable file
693
old code/tray/src/qz/common/TrayManager.java
Executable file
@@ -0,0 +1,693 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.common;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import qz.App;
|
||||
import qz.auth.Certificate;
|
||||
import qz.auth.RequestState;
|
||||
import qz.installer.shortcut.ShortcutCreator;
|
||||
import qz.printer.PrintServiceMatcher;
|
||||
import qz.printer.action.html.WebApp;
|
||||
import qz.ui.*;
|
||||
import qz.ui.component.IconCache;
|
||||
import qz.ui.tray.TrayType;
|
||||
import qz.utils.*;
|
||||
import qz.ws.PrintSocketServer;
|
||||
import qz.ws.SingleInstanceChecker;
|
||||
import qz.ws.WebsocketPorts;
|
||||
import qz.ws.substitutions.Substitutions;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static qz.ui.component.IconCache.Icon.*;
|
||||
import static qz.utils.ArgValue.*;
|
||||
|
||||
/**
|
||||
* Manages the icons and actions associated with the TrayIcon
|
||||
*
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class TrayManager {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(TrayManager.class);
|
||||
|
||||
private boolean headless;
|
||||
|
||||
// The cached icons
|
||||
private final IconCache iconCache;
|
||||
|
||||
// Custom swing pop-up menu
|
||||
private TrayType tray;
|
||||
|
||||
private ConfirmDialog confirmDialog;
|
||||
private GatewayDialog gatewayDialog;
|
||||
private AboutDialog aboutDialog;
|
||||
private LogDialog logDialog;
|
||||
private SiteManagerDialog sitesDialog;
|
||||
private ArrayList<Component> componentList;
|
||||
private IconCache.Icon shownIcon;
|
||||
|
||||
// Need a class reference to this so we can set it from the request dialog window
|
||||
private JCheckBoxMenuItem anonymousItem;
|
||||
|
||||
// The name this UI component will use, i.e "QZ Print 1.9.0"
|
||||
private final String name;
|
||||
|
||||
// The shortcut and startup helper
|
||||
private final ShortcutCreator shortcutCreator;
|
||||
|
||||
private final PropertyHelper prefs;
|
||||
|
||||
// Action to run when reload is triggered
|
||||
private Thread reloadThread;
|
||||
|
||||
// Actions to run if idle after startup
|
||||
private java.util.Timer idleTimer = new java.util.Timer();
|
||||
|
||||
public TrayManager() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a AutoHideJSystemTray with the specified name/text
|
||||
*/
|
||||
public TrayManager(boolean isHeadless) {
|
||||
name = Constants.ABOUT_TITLE + " " + Constants.VERSION;
|
||||
|
||||
prefs = new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
|
||||
prefs.remove(SECURITY_FILE_STRICT.getMatch()); // per https://github.com/qzind/tray/issues/1337
|
||||
|
||||
// Set strict certificate mode preference
|
||||
Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE));
|
||||
|
||||
// Configures JSON websocket messages
|
||||
Substitutions.getInstance();
|
||||
|
||||
// Set FileIO security
|
||||
FileUtilities.setFileIoEnabled(getPref(SECURITY_FILE_ENABLED));
|
||||
FileUtilities.setFileIoStrict(getPref(SECURITY_FILE_STRICT));
|
||||
|
||||
// Headless if turned on by user or unsupported by environment
|
||||
headless = isHeadless || getPref(HEADLESS) || GraphicsEnvironment.isHeadless();
|
||||
if (headless) {
|
||||
log.info("Running in headless mode");
|
||||
}
|
||||
|
||||
// Set up the shortcut name so that the UI components can use it
|
||||
shortcutCreator = ShortcutCreator.getInstance();
|
||||
|
||||
SystemUtilities.setSystemLookAndFeel(headless);
|
||||
iconCache = new IconCache();
|
||||
|
||||
if (SystemUtilities.isSystemTraySupported(headless)) { // UI mode with tray
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
tray = TrayType.JX.init(iconCache);
|
||||
// Undocumented HiDPI behavior
|
||||
tray.setImageAutoSize(true);
|
||||
break;
|
||||
case MAC:
|
||||
tray = TrayType.CLASSIC.init(iconCache);
|
||||
break;
|
||||
default:
|
||||
tray = TrayType.MODERN.init(iconCache);
|
||||
}
|
||||
|
||||
// OS-specific tray icon handling
|
||||
if (SystemTray.isSupported()) {
|
||||
iconCache.fixTrayIcons(SystemUtilities.isDarkTaskbar());
|
||||
}
|
||||
|
||||
// Iterates over all images denoted by IconCache.getTypes() and caches them
|
||||
tray.setIcon(DANGER_ICON);
|
||||
tray.setToolTip(name);
|
||||
|
||||
try {
|
||||
SystemTray.getSystemTray().add(tray.tray());
|
||||
}
|
||||
catch(AWTException awt) {
|
||||
log.error("Could not attach tray, forcing headless mode", awt);
|
||||
headless = true;
|
||||
}
|
||||
} else if (!headless) { // UI mode without tray
|
||||
tray = TrayType.TASKBAR.init(exitListener, iconCache);
|
||||
tray.setIcon(DANGER_ICON);
|
||||
tray.setToolTip(name);
|
||||
tray.showTaskbar();
|
||||
}
|
||||
|
||||
// TODO: Remove when fixed upstream. See issue #393
|
||||
if (SystemUtilities.isUnix() && !isHeadless) {
|
||||
// Update printer list in CUPS immediately (normally 2min)
|
||||
System.setProperty("sun.java2d.print.polling", "false");
|
||||
}
|
||||
|
||||
if (!headless) {
|
||||
componentList = new ArrayList<>();
|
||||
|
||||
// The allow/block dialog
|
||||
gatewayDialog = new GatewayDialog(null, "Action Required", iconCache);
|
||||
componentList.add(gatewayDialog);
|
||||
|
||||
// The ok/cancel dialog
|
||||
confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache);
|
||||
componentList.add(confirmDialog);
|
||||
|
||||
// Detect theme changes
|
||||
new Thread(() -> {
|
||||
boolean darkDesktopMode = SystemUtilities.isDarkDesktop();
|
||||
boolean darkTaskbarMode = SystemUtilities.isDarkTaskbar();
|
||||
while(true) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
if (darkDesktopMode != SystemUtilities.isDarkDesktop(true) ||
|
||||
darkTaskbarMode != SystemUtilities.isDarkTaskbar(true)) {
|
||||
darkDesktopMode = SystemUtilities.isDarkDesktop();
|
||||
darkTaskbarMode = SystemUtilities.isDarkTaskbar();
|
||||
iconCache.fixTrayIcons(darkTaskbarMode);
|
||||
refreshIcon(null);
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
SystemUtilities.setSystemLookAndFeel(headless);
|
||||
for(Component c : componentList) {
|
||||
SwingUtilities.updateComponentTreeUI(c);
|
||||
if (c instanceof Themeable) {
|
||||
((Themeable)c).refresh();
|
||||
}
|
||||
if (c instanceof JDialog) {
|
||||
((JDialog)c).pack();
|
||||
} else if (c instanceof JPopupMenu) {
|
||||
((JPopupMenu)c).pack();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(InterruptedException ignore) {}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
if (tray != null) {
|
||||
addMenuItems();
|
||||
}
|
||||
|
||||
// Initialize idle actions
|
||||
// Slow to start JavaFX the first time
|
||||
if (getPref(TRAY_IDLE_JAVAFX)) {
|
||||
performIfIdle((int)TimeUnit.SECONDS.toMillis(60), evt -> {
|
||||
log.debug("IDLE: Starting up JFX for HTML printing");
|
||||
try {
|
||||
WebApp.initialize();
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.error("Idle runner failed to preemptively start JavaFX service");
|
||||
}
|
||||
});
|
||||
}
|
||||
// Slow to find printers the first time if a lot of printers are installed
|
||||
// Must run after JavaFX per https://github.com/qzind/tray/issues/924
|
||||
if (getPref(TRAY_IDLE_PRINTERS)) {
|
||||
performIfIdle((int)TimeUnit.SECONDS.toMillis(120), evt -> {
|
||||
log.debug("IDLE: Performing first run of find printers");
|
||||
PrintServiceMatcher.getNativePrinterList(false, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stand-alone invocation of TrayManager
|
||||
*
|
||||
* @param args arguments to pass to main
|
||||
*/
|
||||
public static void main(String args[]) {
|
||||
SwingUtilities.invokeLater(TrayManager::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the swing pop-up menu with the specified items
|
||||
*/
|
||||
private void addMenuItems() {
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
componentList.add(popup);
|
||||
|
||||
JMenu advancedMenu = new JMenu("Advanced");
|
||||
advancedMenu.setMnemonic(KeyEvent.VK_A);
|
||||
advancedMenu.setIcon(iconCache.getIcon(SETTINGS_ICON));
|
||||
|
||||
JMenuItem sitesItem = new JMenuItem("Site Manager...", iconCache.getIcon(SAVED_ICON));
|
||||
sitesItem.setMnemonic(KeyEvent.VK_M);
|
||||
sitesItem.addActionListener(savedListener);
|
||||
sitesDialog = new SiteManagerDialog(sitesItem, iconCache, prefs);
|
||||
componentList.add(sitesDialog);
|
||||
|
||||
JMenuItem pairingConfigItem = new JMenuItem("Pairing Configuration...", iconCache.getIcon(SETTINGS_ICON));
|
||||
pairingConfigItem.setMnemonic(KeyEvent.VK_P);
|
||||
pairingConfigItem.addActionListener(e -> new qz.ui.PairingConfigDialog(null).setVisible(true));
|
||||
advancedMenu.add(pairingConfigItem);
|
||||
|
||||
JMenuItem diagnosticMenu = new JMenu("Diagnostic");
|
||||
|
||||
JMenuItem browseApp = new JMenuItem("Browse App folder...", iconCache.getIcon(FOLDER_ICON));
|
||||
browseApp.setToolTipText(SystemUtilities.getJarParentPath().toString());
|
||||
browseApp.setMnemonic(KeyEvent.VK_O);
|
||||
browseApp.addActionListener(e -> ShellUtilities.browseAppDirectory());
|
||||
diagnosticMenu.add(browseApp);
|
||||
|
||||
JMenuItem browseUser = new JMenuItem("Browse User folder...", iconCache.getIcon(FOLDER_ICON));
|
||||
browseUser.setToolTipText(FileUtilities.USER_DIR.toString());
|
||||
browseUser.setMnemonic(KeyEvent.VK_U);
|
||||
browseUser.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.USER_DIR));
|
||||
diagnosticMenu.add(browseUser);
|
||||
|
||||
JMenuItem browseShared = new JMenuItem("Browse Shared folder...", iconCache.getIcon(FOLDER_ICON));
|
||||
browseShared.setToolTipText(FileUtilities.SHARED_DIR.toString());
|
||||
browseShared.setMnemonic(KeyEvent.VK_S);
|
||||
browseShared.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.SHARED_DIR));
|
||||
diagnosticMenu.add(browseShared);
|
||||
|
||||
diagnosticMenu.add(new JSeparator());
|
||||
|
||||
JCheckBoxMenuItem notificationsItem = new JCheckBoxMenuItem("Show all notifications");
|
||||
notificationsItem.setToolTipText("Shows all connect/disconnect messages, useful for debugging purposes");
|
||||
notificationsItem.setMnemonic(KeyEvent.VK_S);
|
||||
notificationsItem.setState(getPref(TRAY_NOTIFICATIONS));
|
||||
notificationsItem.addActionListener(notificationsListener);
|
||||
diagnosticMenu.add(notificationsItem);
|
||||
|
||||
JCheckBoxMenuItem monocleItem = new JCheckBoxMenuItem("Use Monocle for HTML");
|
||||
monocleItem.setToolTipText("Use monocle platform for HTML printing (restart required)");
|
||||
monocleItem.setMnemonic(KeyEvent.VK_U);
|
||||
monocleItem.setState(getPref(TRAY_MONOCLE));
|
||||
if(!SystemUtilities.hasMonocle()) {
|
||||
log.warn("Monocle engine was not detected");
|
||||
monocleItem.setEnabled(false);
|
||||
monocleItem.setToolTipText("Monocle HTML engine was not detected");
|
||||
}
|
||||
monocleItem.addActionListener(monocleListener);
|
||||
|
||||
if (Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("11.0.0"))) { //only include if it can be used
|
||||
diagnosticMenu.add(monocleItem);
|
||||
}
|
||||
|
||||
diagnosticMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem logItem = new JMenuItem("View logs (live feed)...", iconCache.getIcon(LOG_ICON));
|
||||
logItem.setMnemonic(KeyEvent.VK_L);
|
||||
logItem.addActionListener(logListener);
|
||||
diagnosticMenu.add(logItem);
|
||||
logDialog = new LogDialog(logItem, iconCache);
|
||||
componentList.add(logDialog);
|
||||
|
||||
JMenuItem zipLogs = new JMenuItem("Zip logs (to Desktop)");
|
||||
zipLogs.setToolTipText("Zip diagnostic logs, place on Desktop");
|
||||
zipLogs.setMnemonic(KeyEvent.VK_Z);
|
||||
zipLogs.addActionListener(e -> FileUtilities.zipLogs());
|
||||
diagnosticMenu.add(zipLogs);
|
||||
|
||||
JMenuItem desktopItem = new JMenuItem("Create Desktop shortcut", iconCache.getIcon(DESKTOP_ICON));
|
||||
desktopItem.setMnemonic(KeyEvent.VK_D);
|
||||
desktopItem.addActionListener(desktopListener());
|
||||
|
||||
anonymousItem = new JCheckBoxMenuItem("Block anonymous requests");
|
||||
anonymousItem.setToolTipText("Blocks all requests that do not contain a valid certificate/signature");
|
||||
anonymousItem.setMnemonic(KeyEvent.VK_K);
|
||||
anonymousItem.setState(Certificate.UNKNOWN.isBlocked());
|
||||
anonymousItem.addActionListener(anonymousListener);
|
||||
|
||||
if(Constants.ENABLE_DIAGNOSTICS) {
|
||||
advancedMenu.add(diagnosticMenu);
|
||||
advancedMenu.add(new JSeparator());
|
||||
}
|
||||
advancedMenu.add(sitesItem);
|
||||
advancedMenu.add(desktopItem);
|
||||
advancedMenu.add(new JSeparator());
|
||||
advancedMenu.add(anonymousItem);
|
||||
|
||||
JMenuItem reloadItem = new JMenuItem("Reload", iconCache.getIcon(RELOAD_ICON));
|
||||
reloadItem.setMnemonic(KeyEvent.VK_R);
|
||||
reloadItem.addActionListener(reloadListener);
|
||||
|
||||
JMenuItem aboutItem = new JMenuItem("About...", iconCache.getIcon(ABOUT_ICON));
|
||||
aboutItem.setMnemonic(KeyEvent.VK_B);
|
||||
aboutItem.addActionListener(aboutListener);
|
||||
aboutDialog = new AboutDialog(aboutItem, iconCache);
|
||||
componentList.add(aboutDialog);
|
||||
|
||||
if (SystemUtilities.isMac()) {
|
||||
MacUtilities.registerAboutDialog(aboutDialog);
|
||||
MacUtilities.registerQuitHandler(this);
|
||||
}
|
||||
|
||||
JSeparator separator = new JSeparator();
|
||||
|
||||
JCheckBoxMenuItem startupItem = new JCheckBoxMenuItem("Automatically start");
|
||||
startupItem.setMnemonic(KeyEvent.VK_S);
|
||||
startupItem.setState(FileUtilities.isAutostart());
|
||||
startupItem.addActionListener(startupListener());
|
||||
if (!shortcutCreator.canAutoStart()) {
|
||||
startupItem.setEnabled(false);
|
||||
startupItem.setState(false);
|
||||
startupItem.setToolTipText("Autostart has been disabled by the administrator");
|
||||
}
|
||||
|
||||
JMenuItem exitItem = new JMenuItem("Exit", iconCache.getIcon(EXIT_ICON));
|
||||
exitItem.addActionListener(exitListener);
|
||||
|
||||
popup.add(advancedMenu);
|
||||
popup.add(reloadItem);
|
||||
popup.add(aboutItem);
|
||||
popup.add(startupItem);
|
||||
popup.add(separator);
|
||||
popup.add(exitItem);
|
||||
|
||||
if (tray != null) {
|
||||
tray.setJPopupMenu(popup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final ActionListener notificationsListener = new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
prefs.setProperty(TRAY_NOTIFICATIONS, ((JCheckBoxMenuItem)e.getSource()).getState());
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener monocleListener = new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
JCheckBoxMenuItem j = (JCheckBoxMenuItem)e.getSource();
|
||||
prefs.setProperty(TRAY_MONOCLE, j.getState());
|
||||
displayWarningMessage(String.format("A restart of %s is required to ensure this feature is %sabled.",
|
||||
Constants.ABOUT_TITLE, j.getState()? "en":"dis"));
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener desktopListener() {
|
||||
return e -> {
|
||||
shortcutCreator.createDesktopShortcut();
|
||||
};
|
||||
}
|
||||
|
||||
private final ActionListener savedListener = new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
sitesDialog.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener anonymousListener = e -> {
|
||||
boolean checkBoxState = true;
|
||||
if (e.getSource() instanceof JCheckBoxMenuItem) {
|
||||
checkBoxState = ((JCheckBoxMenuItem)e.getSource()).getState();
|
||||
}
|
||||
|
||||
log.debug("Block unsigned: {}", checkBoxState);
|
||||
|
||||
if (checkBoxState) {
|
||||
blackList(Certificate.UNKNOWN);
|
||||
} else {
|
||||
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), true);
|
||||
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), false);
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener logListener = new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
logDialog.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
private ActionListener startupListener() {
|
||||
return e -> {
|
||||
JCheckBoxMenuItem source = (JCheckBoxMenuItem)e.getSource();
|
||||
if (!source.getState() && !confirmDialog.prompt("Remove " + name + " from startup?")) {
|
||||
source.setState(true);
|
||||
return;
|
||||
}
|
||||
if (FileUtilities.setAutostart(source.getState())) {
|
||||
displayInfoMessage("Successfully " + (source.getState() ? "enabled" : "disabled") + " autostart");
|
||||
} else {
|
||||
displayErrorMessage("Error " + (source.getState() ? "enabling" : "disabling") + " autostart");
|
||||
}
|
||||
source.setState(FileUtilities.isAutostart());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default reload action (in this case, <code>Thread.start()</code>) to be fired
|
||||
*
|
||||
* @param reloadThread The Thread to call when reload is clicked
|
||||
*/
|
||||
public void setReloadThread(Thread reloadThread) {
|
||||
this.reloadThread = reloadThread;
|
||||
}
|
||||
|
||||
private ActionListener reloadListener = new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (reloadThread == null) {
|
||||
showErrorDialog("Sorry, Reload has not yet been implemented.");
|
||||
} else {
|
||||
reloadThread.start();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener aboutListener = new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
aboutDialog.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
private final ActionListener exitListener = new ActionListener() {
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
|
||||
if (!showAllNotifications || confirmDialog.prompt("Exit " + name + "?")) { exit(0); }
|
||||
}
|
||||
};
|
||||
|
||||
public void exit(int returnCode) {
|
||||
prefs.save();
|
||||
FileUtilities.cleanup();
|
||||
System.exit(returnCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a basic error dialog.
|
||||
*/
|
||||
private void showErrorDialog(String message) {
|
||||
JOptionPane.showMessageDialog(null, message, name, JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
public boolean showGatewayDialog(final RequestState request, final String prompt, final Point position) {
|
||||
if (!headless) {
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> gatewayDialog.prompt("%s wants to " + prompt, request, position));
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
|
||||
if (gatewayDialog.isApproved()) {
|
||||
log.info("Allowed {} to {}", request.getCertName(), prompt);
|
||||
if (gatewayDialog.isPersistent()) {
|
||||
whiteList(request.getCertUsed());
|
||||
}
|
||||
} else {
|
||||
log.info("Denied {} to {}", request.getCertName(), prompt);
|
||||
if (gatewayDialog.isPersistent()) {
|
||||
if (!request.hasCertificate()) {
|
||||
anonymousItem.doClick(); // if always block anonymous requests -> flag menu item
|
||||
} else {
|
||||
blackList(request.getCertUsed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gatewayDialog.isApproved();
|
||||
} else {
|
||||
return request.hasSavedCert();
|
||||
}
|
||||
}
|
||||
|
||||
private void whiteList(Certificate cert) {
|
||||
if (FileUtilities.printLineToFile(Constants.ALLOW_FILE, cert.data())) {
|
||||
displayInfoMessage(String.format(Constants.ALLOW_SITES_TEXT, cert.getOrganization()));
|
||||
} else {
|
||||
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
|
||||
}
|
||||
}
|
||||
|
||||
private void blackList(Certificate cert) {
|
||||
if (FileUtilities.printLineToFile(Constants.BLOCK_FILE, cert.data())) {
|
||||
displayInfoMessage(String.format(Constants.BLOCK_SITES_TEXT, cert.getOrganization()));
|
||||
} else {
|
||||
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
|
||||
}
|
||||
}
|
||||
|
||||
public void setServer(Server server, WebsocketPorts websocketPorts) {
|
||||
if (server != null && server.getConnectors().length > 0) {
|
||||
singleInstanceCheck(websocketPorts);
|
||||
|
||||
displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
|
||||
|
||||
if (!headless) {
|
||||
aboutDialog.setServer(server);
|
||||
setDefaultIcon();
|
||||
}
|
||||
} else {
|
||||
displayErrorMessage("Invalid server");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread safe method for setting a fine status message. Messages are suppressed unless "Show all
|
||||
* notifications" is checked.
|
||||
*/
|
||||
public void displayInfoMessage(String text) {
|
||||
displayMessage(name, text, TrayIcon.MessageType.INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread safe method for setting the default icon
|
||||
*/
|
||||
public void setDefaultIcon() {
|
||||
// Workaround for JDK-8252015
|
||||
if(SystemUtilities.isMac() && Constants.MASK_TRAY_SUPPORTED && !MacUtilities.jdkSupportsTemplateIcon()) {
|
||||
setIcon(DEFAULT_ICON, () -> MacUtilities.toggleTemplateIcon(tray.tray()));
|
||||
} else {
|
||||
setIcon(DEFAULT_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
/** Thread safe method for setting the error status message */
|
||||
public void displayErrorMessage(String text) {
|
||||
displayMessage(name, text, TrayIcon.MessageType.ERROR);
|
||||
}
|
||||
|
||||
/** Thread safe method for setting the danger icon */
|
||||
public void setDangerIcon() {
|
||||
setIcon(DANGER_ICON);
|
||||
}
|
||||
|
||||
/** Thread safe method for setting the warning status message */
|
||||
public void displayWarningMessage(String text) {
|
||||
displayMessage(name, text, TrayIcon.MessageType.WARNING);
|
||||
}
|
||||
|
||||
/** Thread safe method for setting the warning icon */
|
||||
public void setWarningIcon() {
|
||||
setIcon(WARNING_ICON);
|
||||
}
|
||||
|
||||
/** Thread safe method for setting the specified icon */
|
||||
private void setIcon(final IconCache.Icon i, Runnable whenDone) {
|
||||
if (tray != null && i != shownIcon) {
|
||||
shownIcon = i;
|
||||
refreshIcon(whenDone);
|
||||
}
|
||||
}
|
||||
|
||||
private void setIcon(final IconCache.Icon i) {
|
||||
setIcon(i, null);
|
||||
}
|
||||
|
||||
public void refreshIcon(final Runnable whenDone) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
tray.setIcon(shownIcon);
|
||||
if(whenDone != null) {
|
||||
whenDone.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Thread safe method for setting the specified status message
|
||||
*
|
||||
* @param caption The title of the tray message
|
||||
* @param text The text body of the tray message
|
||||
* @param level The message type: Level.INFO, .WARN, .SEVERE
|
||||
*/
|
||||
private void displayMessage(final String caption, final String text, final TrayIcon.MessageType level) {
|
||||
if (!headless) {
|
||||
if (tray != null) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
|
||||
if (showAllNotifications || level != TrayIcon.MessageType.INFO) {
|
||||
tray.displayMessage(caption, text, level);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.info("{}: [{}] {}", caption, level, text);
|
||||
}
|
||||
}
|
||||
|
||||
public void singleInstanceCheck(WebsocketPorts websocketPorts) {
|
||||
// Secure
|
||||
for(int port : websocketPorts.getUnusedSecurePorts()) {
|
||||
new SingleInstanceChecker(this, port, true);
|
||||
}
|
||||
// Insecure
|
||||
for(int port : websocketPorts.getUnusedInsecurePorts()) {
|
||||
new SingleInstanceChecker(this, port, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMonoclePreferred() {
|
||||
return getPref(TRAY_MONOCLE);
|
||||
}
|
||||
|
||||
public boolean isHeadless() {
|
||||
return headless;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get boolean user pref: Searching "user", "app" and <code>System.getProperty(...)</code>.
|
||||
*/
|
||||
private boolean getPref(ArgValue argValue) {
|
||||
return PrefsSearch.getBoolean(argValue, prefs, App.getTrayProperties());
|
||||
}
|
||||
|
||||
private void performIfIdle(int idleQualifier, ActionListener performer) {
|
||||
if (idleTimer != null) {
|
||||
idleTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
performer.actionPerformed(null);
|
||||
}
|
||||
}, idleQualifier);
|
||||
} else {
|
||||
log.warn("Idle actions have already been cleared due to activity, task not scheduled.");
|
||||
}
|
||||
}
|
||||
|
||||
public void voidIdleActions() {
|
||||
if (idleTimer != null) {
|
||||
log.trace("Not idle, stopping any actions that haven't ran yet");
|
||||
idleTimer.cancel();
|
||||
idleTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
13
old code/tray/src/qz/communication/DeviceException.java
Executable file
13
old code/tray/src/qz/communication/DeviceException.java
Executable file
@@ -0,0 +1,13 @@
|
||||
package qz.communication;
|
||||
|
||||
public class DeviceException extends Exception {
|
||||
|
||||
public DeviceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DeviceException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
29
old code/tray/src/qz/communication/DeviceIO.java
Executable file
29
old code/tray/src/qz/communication/DeviceIO.java
Executable file
@@ -0,0 +1,29 @@
|
||||
package qz.communication;
|
||||
|
||||
public interface DeviceIO extends DeviceListener {
|
||||
|
||||
String getVendorId();
|
||||
|
||||
String getProductId();
|
||||
|
||||
|
||||
void open() throws DeviceException;
|
||||
|
||||
boolean isOpen();
|
||||
|
||||
void close();
|
||||
|
||||
void setStreaming(boolean streaming);
|
||||
|
||||
boolean isStreaming();
|
||||
|
||||
|
||||
byte[] readData(int responseSize, Byte exchangeConfig) throws DeviceException;
|
||||
|
||||
void sendData(byte[] data, Byte exchangeConfig) throws DeviceException;
|
||||
|
||||
|
||||
byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException;
|
||||
|
||||
void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException;
|
||||
}
|
||||
9
old code/tray/src/qz/communication/DeviceListener.java
Executable file
9
old code/tray/src/qz/communication/DeviceListener.java
Executable file
@@ -0,0 +1,9 @@
|
||||
package qz.communication;
|
||||
|
||||
public interface DeviceListener {
|
||||
/**
|
||||
* Cleanup task for when a socket closes while a device is still streaming
|
||||
*/
|
||||
void close();
|
||||
|
||||
}
|
||||
130
old code/tray/src/qz/communication/DeviceOptions.java
Executable file
130
old code/tray/src/qz/communication/DeviceOptions.java
Executable file
@@ -0,0 +1,130 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.utils.UsbUtilities;
|
||||
|
||||
public class DeviceOptions {
|
||||
|
||||
public enum DeviceMode {
|
||||
HID,
|
||||
USB,
|
||||
UNKNOWN;
|
||||
public static DeviceMode parse(String callName) {
|
||||
if (callName != null) {
|
||||
if (callName.startsWith("usb")) {
|
||||
return USB;
|
||||
} else if (callName.startsWith("hid")) {
|
||||
return HID;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceMode deviceMode;
|
||||
|
||||
private Integer vendorId;
|
||||
private Integer productId;
|
||||
|
||||
//usb specific
|
||||
private Byte interfaceId;
|
||||
private Byte endpoint;
|
||||
private int interval;
|
||||
private int responseSize;
|
||||
|
||||
//hid specific
|
||||
private Integer usagePage;
|
||||
private String serial;
|
||||
|
||||
public DeviceOptions(JSONObject parameters, DeviceMode deviceMode) {
|
||||
this.deviceMode = deviceMode;
|
||||
|
||||
vendorId = UsbUtilities.hexToInt(parameters.optString("vendorId"));
|
||||
productId = UsbUtilities.hexToInt(parameters.optString("productId"));
|
||||
|
||||
if (!parameters.isNull("interface")) {
|
||||
interfaceId = UsbUtilities.hexToByte(parameters.optString("interface"));
|
||||
}
|
||||
if (!parameters.isNull("endpoint")) {
|
||||
endpoint = UsbUtilities.hexToByte(parameters.optString("endpoint"));
|
||||
} else if (!parameters.isNull("reportId")) {
|
||||
endpoint = UsbUtilities.hexToByte(parameters.optString("reportId"));
|
||||
}
|
||||
interval = parameters.optInt("interval", 100);
|
||||
responseSize = parameters.optInt("responseSize");
|
||||
|
||||
if (!parameters.isNull("usagePage")) {
|
||||
usagePage = UsbUtilities.hexToInt(parameters.optString("usagePage"));
|
||||
}
|
||||
if (!parameters.isNull("serial")) {
|
||||
serial = parameters.optString("serial", "");
|
||||
serial = serial.isEmpty() ? null : serial;
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getVendorId() {
|
||||
return vendorId;
|
||||
}
|
||||
|
||||
public Integer getProductId() {
|
||||
return productId;
|
||||
}
|
||||
|
||||
public Byte getInterfaceId() {
|
||||
return interfaceId;
|
||||
}
|
||||
|
||||
public Byte getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public int getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public int getResponseSize() {
|
||||
return responseSize;
|
||||
}
|
||||
|
||||
public Integer getUsagePage() {
|
||||
return usagePage;
|
||||
}
|
||||
|
||||
public String getSerial() {
|
||||
return serial;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null || !(obj instanceof DeviceOptions)) { return false; }
|
||||
|
||||
DeviceOptions that = (DeviceOptions)obj;
|
||||
|
||||
if (this.getVendorId().equals(that.getVendorId()) && this.getProductId().equals(that.getProductId())) {
|
||||
if (deviceMode == DeviceMode.USB
|
||||
&& (this.getInterfaceId() == null || that.getInterfaceId() == null || this.getInterfaceId().equals(that.getInterfaceId()))
|
||||
&& (this.getEndpoint() == null || that.getEndpoint() == null || this.getEndpoint().equals(that.getEndpoint()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deviceMode == DeviceMode.HID
|
||||
&& (this.getUsagePage() == null || that.getUsagePage() == null || this.getUsagePage().equals(that.getUsagePage()))
|
||||
&& (this.getSerial() == null || that.getSerial() == null || this.getSerial().equals(that.getSerial()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder()
|
||||
.append(deviceMode)
|
||||
.append(vendorId)
|
||||
.append(productId)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
}
|
||||
173
old code/tray/src/qz/communication/FileIO.java
Executable file
173
old code/tray/src/qz/communication/FileIO.java
Executable file
@@ -0,0 +1,173 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOCase;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import qz.ws.PrintSocketClient;
|
||||
import qz.ws.StreamEvent;
|
||||
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.WatchKey;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class FileIO implements DeviceListener {
|
||||
public static final String SANDBOX_DATA_SUFFIX = "sandbox";
|
||||
public static final String GLOBAL_DATA_SUFFIX = "shared";
|
||||
|
||||
// Pesky breadcrumb files that only the OS cares about
|
||||
public static final String[] DEFAULT_EXCLUSIONS = { ".DS_Store", "Thumbs.db" };
|
||||
|
||||
public static final int FILE_LISTENER_DEFAULT_LINES = 10;
|
||||
|
||||
public enum ReadType {
|
||||
BYTES, LINES
|
||||
}
|
||||
|
||||
private Session session;
|
||||
|
||||
private Path originalPath;
|
||||
private Path absolutePath;
|
||||
|
||||
private WatchKey wk;
|
||||
|
||||
private ReadType readType;
|
||||
private boolean reversed;
|
||||
private long bytes;
|
||||
private int lines;
|
||||
|
||||
private IOCase caseSensitivity;
|
||||
|
||||
private ArrayList<String> inclusions;
|
||||
private ArrayList<String> exclusions;
|
||||
|
||||
public FileIO(Session session, JSONObject params, Path originalPath, Path absolutePath) throws JSONException {
|
||||
this.session = session;
|
||||
this.originalPath = originalPath;
|
||||
this.absolutePath = absolutePath;
|
||||
|
||||
inclusions = new ArrayList<>();
|
||||
exclusions = new ArrayList<>();
|
||||
|
||||
JSONArray inc = params.optJSONArray("include");
|
||||
JSONArray exc = params.optJSONArray("exclude");
|
||||
caseSensitivity = params.optBoolean("ignoreCase", true) ? IOCase.INSENSITIVE : IOCase.SENSITIVE;
|
||||
|
||||
if (inc != null) {
|
||||
for (int i = 0; i < inc.length(); i++) {
|
||||
inclusions.add(inc.getString(i));
|
||||
}
|
||||
}
|
||||
if (exc != null) {
|
||||
for(int i = 0; i < exc.length(); i++) {
|
||||
exclusions.add(exc.getString(i));
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject options = params.optJSONObject("listener");
|
||||
if (options != null) {
|
||||
// Setup defaults
|
||||
bytes = options.optLong("bytes", -1);
|
||||
if (bytes > 0) {
|
||||
readType = ReadType.BYTES;
|
||||
} else {
|
||||
readType = ReadType.LINES;
|
||||
}
|
||||
|
||||
lines = options.optInt("lines", readType == ReadType.LINES? FILE_LISTENER_DEFAULT_LINES:-1);
|
||||
reversed = options.optBoolean("reverse", readType == ReadType.LINES);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMatch(String fileName) {
|
||||
boolean match = inclusions.isEmpty();
|
||||
for (String inclusion : inclusions) {
|
||||
if(FilenameUtils.wildcardMatch(fileName, inclusion, caseSensitivity)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(match) {
|
||||
// Never match on DEFAULT_EXCLUSIONS
|
||||
for(String exclusion : DEFAULT_EXCLUSIONS) {
|
||||
if (FilenameUtils.wildcardMatch(fileName, exclusion, IOCase.INSENSITIVE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(String exclusion : exclusions) {
|
||||
if (FilenameUtils.wildcardMatch(fileName, exclusion, caseSensitivity)) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
public boolean returnsContents() {
|
||||
return bytes > 0 || lines > 0;
|
||||
}
|
||||
|
||||
public ReadType getReadType() {
|
||||
return readType;
|
||||
}
|
||||
|
||||
public boolean isReversed() {
|
||||
return reversed;
|
||||
}
|
||||
|
||||
public long getBytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public int getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
public Path getOriginalPath() {
|
||||
return originalPath;
|
||||
}
|
||||
|
||||
public Path getAbsolutePath() {
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
public boolean isWatching() {
|
||||
return wk != null && wk.isValid();
|
||||
}
|
||||
|
||||
public void setWk(WatchKey wk) {
|
||||
this.wk = wk;
|
||||
}
|
||||
|
||||
public void fileChanged(String fileName, String type, String fileData) throws ClosedChannelException {
|
||||
StreamEvent evt = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ACTION)
|
||||
.withData("file", getOriginalPath().resolve(fileName))
|
||||
.withData("eventType", type);
|
||||
|
||||
if (fileData != null) {
|
||||
evt.withData("fileData", fileData);
|
||||
}
|
||||
|
||||
PrintSocketClient.sendStream(session, evt);
|
||||
}
|
||||
|
||||
public void sendError(String message) throws ClosedChannelException {
|
||||
StreamEvent eventErr = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ERROR)
|
||||
.withData("message", message);
|
||||
PrintSocketClient.sendStream(session, eventErr);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (wk != null) {
|
||||
wk.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
67
old code/tray/src/qz/communication/FileParams.java
Executable file
67
old code/tray/src/qz/communication/FileParams.java
Executable file
@@ -0,0 +1,67 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.PrintingUtilities.Flavor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
/**
|
||||
* Created by Kyle on 2/28/2018.
|
||||
*/
|
||||
public class FileParams {
|
||||
private Path path;
|
||||
private String data;
|
||||
private Flavor flavor;
|
||||
|
||||
private boolean shared;
|
||||
private boolean sandbox;
|
||||
|
||||
private OpenOption appendMode;
|
||||
|
||||
|
||||
public FileParams(JSONObject params) throws JSONException {
|
||||
path = Paths.get(params.getString("path"));
|
||||
data = params.optString("data", "");
|
||||
flavor = Flavor.parse(params, Flavor.PLAIN);
|
||||
|
||||
shared = params.optBoolean("shared", true);
|
||||
sandbox = params.optBoolean("sandbox", true);
|
||||
|
||||
appendMode = params.optBoolean("append")? StandardOpenOption.APPEND:StandardOpenOption.TRUNCATE_EXISTING;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String toString(byte[] bytes) {
|
||||
return ByteUtilities.toString(flavor, bytes);
|
||||
}
|
||||
|
||||
public byte[] getData() throws IOException {
|
||||
return flavor.read(data);
|
||||
}
|
||||
|
||||
public Flavor getFlavor() {
|
||||
return flavor;
|
||||
}
|
||||
|
||||
public boolean isShared() {
|
||||
return shared;
|
||||
}
|
||||
|
||||
public boolean isSandbox() {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
public OpenOption getAppendMode() {
|
||||
return appendMode;
|
||||
}
|
||||
|
||||
}
|
||||
110
old code/tray/src/qz/communication/H4J_HidIO.java
Executable file
110
old code/tray/src/qz/communication/H4J_HidIO.java
Executable file
@@ -0,0 +1,110 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.hid4java.HidDevice;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class H4J_HidIO implements DeviceIO, DeviceListener {
|
||||
|
||||
private HidDevice device;
|
||||
|
||||
private boolean streaming;
|
||||
|
||||
private DeviceOptions dOpts;
|
||||
private SocketConnection websocket;
|
||||
|
||||
|
||||
public H4J_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this(H4J_HidUtilities.findDevice(dOpts), dOpts, websocket);
|
||||
}
|
||||
|
||||
private H4J_HidIO(HidDevice device, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this.dOpts = dOpts;
|
||||
this.websocket = websocket;
|
||||
if (device == null) {
|
||||
throw new DeviceException("HID device could not be found");
|
||||
}
|
||||
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public void open() {
|
||||
if (!isOpen()) {
|
||||
device.open();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return !device.isClosed();
|
||||
}
|
||||
|
||||
public void setStreaming(boolean active) {
|
||||
streaming = active;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return streaming;
|
||||
}
|
||||
|
||||
public String getVendorId() {
|
||||
return UsbUtil.toHexString(device.getVendorId());
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return UsbUtil.toHexString(device.getProductId());
|
||||
}
|
||||
|
||||
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
|
||||
byte[] response = new byte[responseSize];
|
||||
|
||||
int read = device.read(response);
|
||||
if (read == -1) {
|
||||
throw new DeviceException("Failed to read from device");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void sendData(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
|
||||
int wrote = device.write(data, data.length, reportId);
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
byte[] response = new byte[responseSize];
|
||||
|
||||
int read = device.getFeatureReport(response, reportId);
|
||||
if (read == -1) {
|
||||
throw new DeviceException("Failed to read from device");
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
|
||||
int wrote = device.sendFeatureReport(data, reportId);
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setStreaming(false);
|
||||
// Remove orphaned reference
|
||||
websocket.removeDevice(dOpts);
|
||||
if (isOpen()) {
|
||||
device.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
83
old code/tray/src/qz/communication/H4J_HidListener.java
Executable file
83
old code/tray/src/qz/communication/H4J_HidListener.java
Executable file
@@ -0,0 +1,83 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.hid4java.HidDevice;
|
||||
import org.hid4java.HidManager;
|
||||
import org.hid4java.HidServicesListener;
|
||||
import org.hid4java.event.HidServicesEvent;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.ws.PrintSocketClient;
|
||||
import qz.ws.StreamEvent;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class H4J_HidListener implements DeviceListener, HidServicesListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(H4J_HidListener.class);
|
||||
|
||||
private Session session;
|
||||
|
||||
|
||||
public H4J_HidListener(Session session) {
|
||||
HidManager.getHidServices().addHidServicesListener(this);
|
||||
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void hidFailure(HidServicesEvent hidServicesEvent) {
|
||||
log.debug("Device failure: {}", hidServicesEvent.getHidDevice().getProduct());
|
||||
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Failure"), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDataReceived(HidServicesEvent hidServicesEvent) {
|
||||
log.debug("Data received: {}", hidServicesEvent.getDataReceived().length + " bytes");
|
||||
|
||||
JSONArray hex = new JSONArray();
|
||||
for(byte b : hidServicesEvent.getDataReceived()) {
|
||||
hex.put(UsbUtil.toHexString(b));
|
||||
}
|
||||
|
||||
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Data Received", hex), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceDetached(HidServicesEvent hidServicesEvent) {
|
||||
log.debug("Device detached: {}", hidServicesEvent.getHidDevice().getProduct());
|
||||
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Detached"), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hidDeviceAttached(HidServicesEvent hidServicesEvent) {
|
||||
log.debug("Device attached: {}", hidServicesEvent.getHidDevice().getProduct());
|
||||
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Attached"), this);
|
||||
}
|
||||
|
||||
private StreamEvent createStreamAction(HidDevice device, String action) {
|
||||
return createStreamAction(device, action, null);
|
||||
}
|
||||
|
||||
private StreamEvent createStreamAction(HidDevice device, String action, JSONArray dataArr) {
|
||||
StreamEvent event = new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
|
||||
.withData("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||
.withData("productId", UsbUtil.toHexString(device.getProductId()))
|
||||
.withData("actionType", action);
|
||||
|
||||
if (dataArr != null) {
|
||||
event.withData("data", dataArr);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
HidManager.getHidServices().removeHidServicesListener(this);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user