diff --git a/ch.eitchnet.utils/LICENSE b/ch.eitchnet.utils/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/ch.eitchnet.utils/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ch.eitchnet.utils/README.md b/ch.eitchnet.utils/README.md
new file mode 100644
index 000000000..46c7a0bcd
--- /dev/null
+++ b/ch.eitchnet.utils/README.md
@@ -0,0 +1,47 @@
+ch.eitchnet.utils
+======================
+
+[![Build Status](http://jenkins.eitchnet.ch/buildStatus/icon?job=ch.eitchnet.utils)](http://jenkins.eitchnet.ch/view/ch.eitchnet/job/ch.eitchnet.utils/)
+
+Java Utilites which ease daily work when programming in the Java language
+
+Dependencies
+----------------------
+This utility package is built by Maven3 and has very few external dependencies. The current dependencies are:
+* the Java Runtime Environment 7
+* JUnit 4.11 (test scope)
+* slf4j 1.7.2
+* slf4j-log4j bindings (test scope)
+
+Features
+----------------------
+* RMI File client/server
+ * This is a small RMI client server which allows to fetch files from a server which exposes the RmiFileHandler class via RMI
+* ObjectFilter
+ * The ObjectFilter allows to keep track of modifications to objects. The modifications are add/update/remove.
+ * You register the modification of an object on the filter and when all is done, you query the filter for all the add/update/remove modifications so that you only persist the required changes to your database
+* ArraysHelper
+ * The ArraysHelper contains methods to handling arrays
+* BaseEncoding
+ * The BaseEncoding class implements RFC4648 and thus implements Base64, Base32, Base16 in all their different alphabets and also implementes the D-Base32 encoding
+* ByteHelper
+ * The ByteHelper contains methods to print, convert and manipulate bytes
+* FileHelper
+ * The FileHelper contains methods relevant to files. E.g. recursively deleting directories, copying files, reading/writing files etc.
+* ProcessHelper
+ * The ProcessHelper abstracts away OS specific process tasks
+* StringHelper
+ * The StringHelper contains methods for handling Strings
+* SystemHelper
+ * The SystemHelper contains methods to get system specific information
+* XmlHelper
+ * The XmlHelper contains methods to handle XML files
+
+Building
+-------------------------
+* Prerequisites:
+ * JDK 6 is installed and JAVA_HOME is properly set and ../bin is in path
+ * Maven 3 is installed and MAVEN_HOME is properly set and ../bin is in path
+* Clone repository and change path to root
+* Run maven:
+ * mvn clean install
diff --git a/ch.eitchnet.utils/pom.xml b/ch.eitchnet.utils/pom.xml
new file mode 100644
index 000000000..9839d0b9a
--- /dev/null
+++ b/ch.eitchnet.utils/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+
+ ch.eitchnet
+ ch.eitchnet.parent
+ 1.1.0-SNAPSHOT
+ ../ch.eitchnet.parent/pom.xml
+
+
+ ch.eitchnet.utils
+ jar
+ ch.eitchnet.utils
+ These utils contain project independent helper classes and utilities for reuse
+ https://github.com/eitchnet/ch.eitchnet.utils
+ 2011
+
+
+ Github Issues
+ https://github.com/eitchnet/ch.eitchnet.utils/issues
+
+
+
+ scm:git:https://github.com/eitchnet/ch.eitchnet.utils.git
+ scm:git:git@github.com:eitchnet/ch.eitchnet.utils.git
+ https://github.com/eitchnet/ch.eitchnet.utils
+ HEAD
+
+
+
+
+ org.slf4j
+ slf4j-log4j12
+ runtime
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-eclipse-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+
+
+
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommandKey.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommandKey.java
new file mode 100644
index 000000000..f34389bf0
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommandKey.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * @author Robert von Burg
+ */
+public class CommandKey {
+ private final String key1;
+ private final String key2;
+ private final int hashCode;
+
+ /**
+ * @param key1
+ * @param key2
+ */
+ public CommandKey(String key1, String key2) {
+ this.key1 = key1;
+ this.key2 = key2;
+
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.key1 == null) ? 0 : this.key1.hashCode());
+ result = prime * result + ((this.key2 == null) ? 0 : this.key2.hashCode());
+ this.hashCode = result;
+ }
+
+ public static CommandKey key(String key1, String key2) {
+ return new CommandKey(key1, key2);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ CommandKey other = (CommandKey) obj;
+ return this.key1.equals(other.key1) && this.key2.equals(other.key2);
+ }
+
+ @Override
+ public String toString() {
+ return this.key1 + StringHelper.COLON + this.key2;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationConnection.java
new file mode 100644
index 000000000..46db7c6ba
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationConnection.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.IoMessage.State;
+import ch.eitchnet.utils.collections.MapOfLists;
+import ch.eitchnet.utils.dbc.DBC;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * @author Robert von Burg
+ */
+public class CommunicationConnection implements Runnable {
+
+ protected static final Logger logger = LoggerFactory.getLogger(CommunicationConnection.class);
+
+ private String id;
+ private ConnectionMode mode;
+ private Map parameters;
+
+ private ConnectionState state;
+ private String stateMsg;
+
+ private BlockingDeque messageQueue;
+ private Thread queueThread;
+ private volatile boolean run;
+ private MapOfLists connectionObservers;
+ private List connectionStateObservers;
+
+ private CommunicationEndpoint endpoint;
+ private IoMessageVisitor messageVisitor;
+
+ private IoMessageArchive archive;
+
+ public CommunicationConnection(String id, ConnectionMode mode, Map parameters,
+ CommunicationEndpoint endpoint, IoMessageVisitor messageVisitor) {
+
+ DBC.PRE.assertNotEmpty("Id must be set!", id); //$NON-NLS-1$
+ DBC.PRE.assertNotNull("ConnectionMode must be set!", mode); //$NON-NLS-1$
+ DBC.PRE.assertNotNull("Paramerters must not be null!", parameters); //$NON-NLS-1$
+ DBC.PRE.assertNotNull("Endpoint must be set!", endpoint); //$NON-NLS-1$
+ DBC.PRE.assertNotNull("IoMessageVisitor must be set!", messageVisitor); //$NON-NLS-1$
+
+ this.id = id;
+ this.mode = mode;
+ this.parameters = parameters;
+ this.endpoint = endpoint;
+ this.messageVisitor = messageVisitor;
+
+ this.state = ConnectionState.CREATED;
+ this.stateMsg = this.state.toString();
+ this.messageQueue = new LinkedBlockingDeque<>();
+ this.connectionObservers = new MapOfLists<>();
+ this.connectionStateObservers = new ArrayList<>();
+ }
+
+ public void setArchive(IoMessageArchive archive) {
+ this.archive = archive;
+ }
+
+ public IoMessageArchive getArchive() {
+ return this.archive;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public int getQueueSize() {
+ return this.messageQueue.size();
+ }
+
+ public ConnectionState getState() {
+ return this.state;
+ }
+
+ public String getStateMsg() {
+ return this.stateMsg;
+ }
+
+ public ConnectionMode getMode() {
+ return this.mode;
+ }
+
+ public Map getParameters() {
+ return this.parameters;
+ }
+
+ public void clearQueue() {
+ this.messageQueue.clear();
+ }
+
+ public void addConnectionObserver(CommandKey key, ConnectionObserver observer) {
+ synchronized (this.connectionObservers) {
+ this.connectionObservers.addElement(key, observer);
+ }
+ }
+
+ public void removeConnectionObserver(CommandKey key, ConnectionObserver observer) {
+ synchronized (this.connectionObservers) {
+ this.connectionObservers.removeElement(key, observer);
+ }
+ }
+
+ public void addConnectionStateObserver(ConnectionStateObserver observer) {
+ synchronized (this.connectionStateObservers) {
+ this.connectionStateObservers.add(observer);
+ }
+ }
+
+ public void removeConnectionStateObserver(ConnectionStateObserver observer) {
+ synchronized (this.connectionStateObservers) {
+ this.connectionStateObservers.remove(observer);
+ }
+ }
+
+ public void notifyStateChange(ConnectionState state, String stateMsg) {
+ ConnectionState oldState = this.state;
+ String oldStateMsg = this.stateMsg;
+ this.state = state;
+ this.stateMsg = stateMsg;
+
+ List observers;
+ synchronized (this.connectionStateObservers) {
+ observers = new ArrayList<>(this.connectionStateObservers);
+ }
+ for (ConnectionStateObserver observer : observers) {
+ observer.notify(oldState, oldStateMsg, state, stateMsg);
+ }
+ }
+
+ public void switchMode(ConnectionMode mode) {
+ ConnectionMessages.assertConfigured(this, "Can not switch modes yet!"); //$NON-NLS-1$
+ if (mode == ConnectionMode.OFF) {
+ stop();
+ } else if (mode == ConnectionMode.ON) {
+ stop();
+ start();
+ }
+
+ this.mode = mode;
+ }
+
+ /**
+ * Configure the underlying {@link CommunicationEndpoint} and {@link IoMessageVisitor}
+ */
+ public void configure() {
+ this.messageVisitor.configure(this);
+ this.endpoint.configure(this, this.messageVisitor);
+ notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.name());
+ }
+
+ public void start() {
+ ConnectionMessages.assertConfigured(this, "Can not start yet!"); //$NON-NLS-1$
+
+ switch (this.mode) {
+ case OFF:
+ logger.info("Not connecting as mode is currently OFF"); //$NON-NLS-1$
+ break;
+ case SIMULATION:
+ logger.info("Started SIMULATION connection!"); //$NON-NLS-1$
+ break;
+ case ON:
+ if (this.queueThread != null) {
+ logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$
+ } else {
+ logger.info(MessageFormat.format("Starting Connection {0} to {1}...", this.id, getRemoteUri())); //$NON-NLS-1$
+ this.run = true;
+ this.queueThread = new Thread(this, MessageFormat.format("{0}_OUT", this.id)); //$NON-NLS-1$
+ this.queueThread.start();
+
+ connectEndpoint();
+ }
+ break;
+ default:
+ logger.error("Unhandled mode " + this.mode); //$NON-NLS-1$
+ break;
+ }
+ }
+
+ public void stop() {
+ ConnectionMessages.assertConfigured(this, "Can not stop yet!"); //$NON-NLS-1$
+
+ switch (this.mode) {
+ case OFF:
+ break;
+ case SIMULATION:
+ logger.info("Disconnected SIMULATION connection!"); //$NON-NLS-1$
+ break;
+ case ON:
+ logger.info("Disconnecting..."); //$NON-NLS-1$
+ if (this.queueThread == null) {
+ logger.warn(MessageFormat.format("{0}: Already disconnected!", this.id)); //$NON-NLS-1$
+ } else {
+ this.run = false;
+
+ try {
+ disconnectEndpoint();
+ } catch (Exception e) {
+ String msg = "Caught exception while disconnecting endpoint: {0}"; //$NON-NLS-1$
+ logger.error(MessageFormat.format(msg, e.getLocalizedMessage()), e);
+ }
+
+ try {
+ this.queueThread.interrupt();
+ } catch (Exception e) {
+ String msg = "Caught exception while stopping queue thread: {0}"; //$NON-NLS-1$
+ logger.warn(MessageFormat.format(msg, e.getLocalizedMessage()));
+ }
+ String msg = "{0} is stopped"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.queueThread.getName()));
+ this.queueThread = null;
+ }
+ break;
+ default:
+ logger.error("Unhandled mode " + this.mode); //$NON-NLS-1$
+ break;
+ }
+ }
+
+ /**
+ * Called by the underlying endpoint when a new message has been received and parsed
+ *
+ * @param message
+ */
+ public void handleNewMessage(IoMessage message) {
+ ConnectionMessages.assertConfigured(this, "Can not be notified of new message yet!"); //$NON-NLS-1$
+
+ // if the state of the message is already later than ACCEPTED
+ // then an underlying component has already set the state, so
+ // we don't need to set it
+ if (message.getState().compareTo(State.ACCEPTED) < 0)
+ message.setState(State.ACCEPTED, StringHelper.DASH);
+
+ notifyObservers(message);
+ }
+
+ public void notifyObservers(IoMessage message) {
+ List observers;
+ synchronized (this.connectionObservers) {
+ List list = this.connectionObservers.getList(message.getKey());
+ if (list == null)
+ return;
+
+ observers = new ArrayList<>(list);
+ }
+
+ for (ConnectionObserver observer : observers) {
+ try {
+ observer.notify(message.getKey(), message);
+ } catch (Exception e) {
+ String msg = "Failed to notify observer for key {0} on message with id {1}"; //$NON-NLS-1$
+ logger.error(MessageFormat.format(msg, message.getKey(), message.getId()), e);
+ }
+ }
+
+ if (this.archive != null)
+ this.archive.archive(message);
+ }
+
+ @Override
+ public void run() {
+ while (this.run) {
+
+ IoMessage message = null;
+
+ try {
+
+ message = this.messageQueue.take();
+ logger.info(MessageFormat.format("Processing message {0}...", message.getId())); //$NON-NLS-1$
+
+ if (this.mode == ConnectionMode.ON)
+ this.endpoint.send(message);
+ else if (this.mode == ConnectionMode.SIMULATION)
+ this.endpoint.simulate(message);
+
+ // notify the caller that the message has been processed
+ if (message.getState().compareTo(State.DONE) < 0)
+ message.setState(State.DONE, StringHelper.DASH);
+
+ done(message);
+
+ } catch (InterruptedException e) {
+ logger.warn(MessageFormat.format("{0} connection has been interruped!", this.id)); //$NON-NLS-1$
+
+ // an interrupted exception means the thread must stop
+ this.run = false;
+
+ if (message != null) {
+ logger.error(MessageFormat.format("Can not send message {0}", message.getId())); //$NON-NLS-1$
+ message.setState(State.FATAL, e.getLocalizedMessage());
+ done(message);
+ }
+
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+
+ if (message != null) {
+ logger.error(MessageFormat.format("Can not send message {0}", message.getId())); //$NON-NLS-1$
+ message.setState(State.FATAL, e.getLocalizedMessage());
+ done(message);
+ }
+ } finally {
+ if (message != null && this.archive != null) {
+ this.archive.archive(message);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when an outgoing message has been handled. This method logs the message state and then notifies all
+ * observers
+ *
+ * @param message
+ */
+ public void done(IoMessage message) {
+ ConnectionMessages.assertConfigured(this, "Can not notify observers yet!"); //$NON-NLS-1$
+
+ switch (message.getState()) {
+ case ACCEPTED:
+ case CREATED:
+ case DONE:
+ case PENDING:
+ logger.info(MessageFormat.format("Sent message {0}", message.toString())); //$NON-NLS-1$
+ break;
+ case FAILED:
+ case FATAL:
+ logger.error(MessageFormat.format("Failed to send message {0}", message.toString())); //$NON-NLS-1$
+ break;
+ default:
+ logger.error(MessageFormat.format("Unhandled state for message {0}", message.toString())); //$NON-NLS-1$
+ break;
+ }
+
+ notifyObservers(message);
+ }
+
+ public String getRemoteUri() {
+ return this.endpoint == null ? "0.0.0.0:0" : this.endpoint.getRemoteUri(); //$NON-NLS-1$
+ }
+
+ public String getLocalUri() {
+ return this.endpoint == null ? "0.0.0.0:0" : this.endpoint.getLocalUri(); //$NON-NLS-1$
+ }
+
+ public void reset() {
+ ConnectionMessages.assertConfigured(this, "Can not resest yet!"); //$NON-NLS-1$
+ this.endpoint.reset();
+ }
+
+ /**
+ * Called when the connection is connected, thus the underlying endpoint can be started
+ */
+ protected void connectEndpoint() {
+ this.endpoint.start();
+ }
+
+ /**
+ * Called when the connection is disconnected, thus the underlying endpoint must be stopped
+ */
+ protected void disconnectEndpoint() {
+ this.endpoint.stop();
+ }
+
+ /**
+ * Send the message using the underlying endpoint. Do not change the state of the message, this will be done by the
+ * caller
+ *
+ * @param message
+ */
+ public void send(IoMessage message) {
+ ConnectionMessages.assertConfigured(this, "Can not send yet"); //$NON-NLS-1$
+ if (this.mode == ConnectionMode.OFF)
+ throw ConnectionMessages.throwNotConnected(this, message);
+
+ message.setState(State.PENDING, State.PENDING.name());
+
+ this.messageQueue.add(message);
+ this.messageQueue.add(message);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java
new file mode 100644
index 000000000..54579f7b9
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+/**
+ * @author Robert von Burg
+ */
+public interface CommunicationEndpoint {
+
+ public void configure(CommunicationConnection connection, IoMessageVisitor converter);
+
+ public void start();
+
+ public void stop();
+
+ public void reset();
+
+ public String getLocalUri();
+
+ public String getRemoteUri();
+
+ public void send(IoMessage message) throws Exception;
+
+ public void simulate(IoMessage message) throws Exception;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionException.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionException.java
new file mode 100644
index 000000000..465477567
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+/**
+ * Base Exception for exceptions thrown by the communication classes
+ *
+ * @author Robert von Burg
+ */
+public class ConnectionException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public ConnectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ConnectionException(String message) {
+ super(message);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionInfo.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionInfo.java
new file mode 100644
index 000000000..c5bfd51a3
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionInfo.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2012, Robert von Burg
+ *
+ * All rights reserved.
+ *
+ * This file is part of the XXX.
+ *
+ * XXX is free software: you can redistribute
+ * it and/or modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * XXX 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XXX. If not, see
+ * .
+ */
+package ch.eitchnet.communication;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * @author Robert von Burg
+ */
+@XmlRootElement(name = "ConnectionInfo")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class ConnectionInfo {
+
+ @XmlAttribute(name = "id")
+ private String id;
+
+ @XmlAttribute(name = "localUri")
+ private String localUri;
+
+ @XmlAttribute(name = "remoteUri")
+ private String remoteUri;
+
+ @XmlAttribute(name = "mode")
+ private ConnectionMode mode;
+
+ @XmlAttribute(name = "queueSize")
+ private int queueSize;
+
+ @XmlAttribute(name = "state")
+ private ConnectionState state;
+
+ @XmlAttribute(name = "stateMsg")
+ private String stateMsg;
+
+ /**
+ * @return the id
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * @param id
+ * the id to set
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * @return the localUri
+ */
+ public String getLocalUri() {
+ return this.localUri;
+ }
+
+ /**
+ * @param localUri
+ * the localUri to set
+ */
+ public void setLocalUri(String localUri) {
+ this.localUri = localUri;
+ }
+
+ /**
+ * @return the remoteUri
+ */
+ public String getRemoteUri() {
+ return this.remoteUri;
+ }
+
+ /**
+ * @param remoteUri
+ * the remoteUri to set
+ */
+ public void setRemoteUri(String remoteUri) {
+ this.remoteUri = remoteUri;
+ }
+
+ /**
+ * @return the mode
+ */
+ public ConnectionMode getMode() {
+ return this.mode;
+ }
+
+ /**
+ * @param mode
+ * the mode to set
+ */
+ public void setMode(ConnectionMode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * @return the state
+ */
+ public ConnectionState getState() {
+ return this.state;
+ }
+
+ /**
+ * @param state
+ * the state to set
+ */
+ public void setState(ConnectionState state) {
+ this.state = state;
+ }
+
+ /**
+ * @return the stateMsg
+ */
+ public String getStateMsg() {
+ return this.stateMsg;
+ }
+
+ /**
+ * @param stateMsg
+ * the stateMsg to set
+ */
+ public void setStateMsg(String stateMsg) {
+ this.stateMsg = stateMsg;
+ }
+
+ /**
+ * @return the queueSize
+ */
+ public int getQueueSize() {
+ return this.queueSize;
+ }
+
+ /**
+ * @param queueSize
+ * the queueSize to set
+ */
+ public void setQueueSize(int queueSize) {
+ this.queueSize = queueSize;
+ }
+
+ public static ConnectionInfo valueOf(CommunicationConnection connection) {
+ ConnectionInfo info = new ConnectionInfo();
+ info.setId(connection.getId());
+ info.setLocalUri(connection.getLocalUri());
+ info.setRemoteUri(connection.getRemoteUri());
+ info.setMode(connection.getMode());
+ info.setState(connection.getState());
+ info.setStateMsg(connection.getStateMsg());
+ info.setQueueSize(connection.getQueueSize());
+ return info;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMessages.java
new file mode 100644
index 000000000..28f6baef1
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMessages.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * Helper class to thrown connection messages
+ *
+ * @author Robert von Burg
+ */
+public class ConnectionMessages {
+
+ private static Logger logger = LoggerFactory.getLogger(ConnectionMessages.class);
+
+ /**
+ * Utility class
+ */
+ private ConnectionMessages() {
+ //
+ }
+
+ /**
+ * Convenience method to throw an exception when an illegal {@link ConnectionState} change occurs
+ *
+ * @param current
+ * @param change
+ *
+ * @return
+ */
+ public static ConnectionException throwIllegalConnectionState(ConnectionState current, ConnectionState change) {
+ String msg = "The connection with state {0} cannot be changed to {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, current.name(), change.name());
+ ConnectionException e = new ConnectionException(msg);
+ return e;
+ }
+
+ /**
+ * Convenience method to throw an exception when an invalid parameter is set in the configuration
+ *
+ * @param clazz
+ * @param parameterName
+ * @param parameterValue
+ *
+ * @return
+ */
+ public static ConnectionException throwInvalidParameter(Class> clazz, String parameterName, String parameterValue) {
+ String value;
+ if (parameterValue != null && !parameterValue.isEmpty())
+ value = parameterValue;
+ else
+ value = StringHelper.NULL;
+
+ String msg = "{0}: parameter ''{1}'' has invalid value ''{2}''"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, clazz.getSimpleName(), parameterName, value);
+ ConnectionException e = new ConnectionException(msg);
+ return e;
+ }
+
+ /**
+ * Convenience method to throw an exception when an two conflicting parameters are activated
+ *
+ * @param clazz
+ * @param parameter1
+ * @param parameter2
+ *
+ * @return
+ */
+ public static ConnectionException throwConflictingParameters(Class> clazz, String parameter1, String parameter2) {
+ String msg = "{0} : The parameters {1} and {2} can not be both activated as they conflict"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, clazz.getSimpleName(), parameter1, parameter1);
+ ConnectionException e = new ConnectionException(msg);
+ return e;
+ }
+
+ /**
+ * Convenience method to log a warning when a parameter is not set in the configuration
+ *
+ * @param clazz
+ * @param parameterName
+ * @param defValue
+ */
+ public static void warnUnsetParameter(Class> clazz, String parameterName, String defValue) {
+ if (logger.isDebugEnabled()) {
+ String msg = "{0}: parameter ''{1}'' is not set, using default value ''{2}''"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, clazz.getSimpleName(), parameterName, defValue);
+ Map properties = new HashMap<>();
+ logger.warn(MessageFormat.format(msg, properties));
+ }
+ }
+
+ /**
+ * Convenience method to throw an exception when the connection is not yet configured
+ *
+ * @param connection
+ * @param message
+ */
+ public static void assertConfigured(CommunicationConnection connection, String message) {
+ if (connection.getState() == ConnectionState.CREATED) {
+ String msg = "{0} : Not yet configured: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, connection.getId(), message);
+ throw new ConnectionException(msg);
+ }
+ }
+
+ /**
+ * Convenience method to throw an exception when the connection is not connected
+ *
+ * @param connection
+ * @param message
+ *
+ * @return
+ */
+ public static ConnectionException throwNotConnected(CommunicationConnection connection, String message) {
+ String msg = "{0} : Not connected: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, connection.getId(), message);
+ ConnectionException e = new ConnectionException(msg);
+ return e;
+ }
+
+ /**
+ * Convenience method to throw an exception when the connection is not connected
+ *
+ * @param connection
+ * @param message
+ *
+ * @return
+ */
+ public static ConnectionException throwNotConnected(CommunicationConnection connection, IoMessage message) {
+ String msg = "{0} : Not connected, can not send message with id {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, connection.getId(), message.getId());
+ ConnectionException e = new ConnectionException(msg);
+ return e;
+ }
+
+ public static void assertLegalMessageVisitor(Class extends CommunicationEndpoint> endpoint,
+ Class extends IoMessageVisitor> expectedVisitor, IoMessageVisitor actualVisitor) {
+ if (!(expectedVisitor.isAssignableFrom(actualVisitor.getClass()))) {
+ String msg = "{0} requires {1} but has received illegal type {2}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, endpoint.getName(), expectedVisitor.getName(), actualVisitor.getClass()
+ .getName());
+ throw new ConnectionException(msg);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMode.java
new file mode 100644
index 000000000..8cf2e2640
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMode.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.io.IOException;
+
+/**
+ *
+ * The mode of an {@link CommunicationConnection} can be one of the following enum values. This makes it possible use
+ * the connection without starting connection and later starting the connection when required
+ *
+ * The modes have the following semantics:
+ *
+ *
OFF - the connection can only have states {@link ConnectionState#CREATED} and {@link ConnectionState#INITIALIZED}
+ * . Trying to use the connection will throw an exception
+ *
ON - the connection can be used normally
+ *
SIMULATION - the same as ON, with the difference that the connection should silently drop any work
+ *
+ *
+ * @author Robert von Burg
+ */
+public enum ConnectionMode {
+
+ /**
+ * Denotes that the {@link CommunicationConnection} is off. This means it cannot accept messages, process messages
+ * or do any other kind of work
+ */
+ OFF {
+ @Override
+ public boolean isSimulation() {
+ return false;
+ }
+ },
+
+ /**
+ * Denotes that the {@link CommunicationConnection} is on. This means that the {@link CommunicationConnection}
+ * accepts and process messages. Any connections which need to be established will automatically be connected and
+ * re-established should an {@link IOException} occur
+ */
+ ON {
+ @Override
+ public boolean isSimulation() {
+ return false;
+ }
+ },
+
+ /**
+ * Denotes that the {@link CommunicationConnection} is in simulation mode. Mostly this means that the
+ * {@link CommunicationConnection} accepts messages, but silently swallows them, instead of processing them
+ */
+ SIMULATION {
+ @Override
+ public boolean isSimulation() {
+ return true;
+ }
+ };
+
+ /**
+ * @return true if the current mode is simulation, false otherwise
+ */
+ public abstract boolean isSimulation();
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionObserver.java
new file mode 100644
index 000000000..8445ca357
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionObserver.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+/**
+ * @author Robert von Burg
+ */
+public interface ConnectionObserver {
+
+ public void notify(CommandKey key, IoMessage message);
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionState.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionState.java
new file mode 100644
index 000000000..13724331e
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionState.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+/**
+ *
+ * a {@link CommunicationConnection} undergoes a serious of state changes. These states can be viewed by a client to
+ * monitor the state of the connection
+ *
+ * The states have the following semantics:
+ *
+ *
CREATED - initial state
+ *
INITIALIZED - the appropriate connection parameters are found
+ *
CONNECTING - the connection is trying to build up a connection
+ *
WAITING - the connection is waiting before retrying to connect
+ *
CONNECTED - the connection has just been established
+ *
IDLE - the connection is connected, but waiting for work
+ *
WORKING - the connection is working
+ *
BROKEN - the connection has lost the connection and is waiting before reconnecting, or another unknown failure
+ * occurred
+ *
DISCONNECTED - the connection has been disconnected
+ *
+ *
+ * @author Robert von Burg
+ */
+public enum ConnectionState {
+
+ // initial
+ CREATED,
+ // configured and ready to connect
+ INITIALIZED,
+ // working
+ CONNECTING,
+ WAITING,
+ CONNECTED,
+ IDLE,
+ WORKING,
+ BROKEN,
+ // disconnected due to connection error or manual disconnect/stop
+ DISCONNECTED;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java
new file mode 100644
index 000000000..3b98f1f17
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java
@@ -0,0 +1,6 @@
+package ch.eitchnet.communication;
+
+public interface ConnectionStateObserver {
+
+ public void notify(ConnectionState oldState, String oldStateMsg, ConnectionState newState, String newStateMsg);
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessage.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessage.java
new file mode 100644
index 000000000..45d2ba77e
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessage.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import ch.eitchnet.utils.helper.StringHelper;
+import ch.eitchnet.utils.iso8601.ISO8601FormatFactory;
+
+/**
+ *
+ * An {@link IoMessage} is the object containing the data to be transmitted in IO. Implementations of
+ * {@link CommunicationConnection} should implement sub classes of this method to hold the actual payload.
+ *
+ *
+ *
+ * This class also contains a {@link Map} to store transient meta data to the actual payload
+ *
+ *
+ * @author Robert von Burg
+ */
+public class IoMessage {
+
+ private final String id;
+ private final CommandKey key;
+ private final String connectionId;
+ private Date updated;
+ private State state;
+ private String stateMsg;
+ private Map parameters;
+
+ /**
+ * @param id
+ * @param key
+ * @param connectionId
+ */
+ public IoMessage(String id, CommandKey key, String connectionId) {
+ this.id = id;
+ this.key = key;
+ this.connectionId = connectionId;
+ this.state = State.CREATED;
+ this.stateMsg = StringHelper.DASH;
+ this.updated = new Date();
+ this.parameters = new HashMap<>();
+ }
+
+ /**
+ * @return the id
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * @return the key
+ */
+ public CommandKey getKey() {
+ return this.key;
+ }
+
+ /**
+ * @return the connectionId
+ */
+ public String getConnectionId() {
+ return this.connectionId;
+ }
+
+ /**
+ * @return the updated
+ */
+ public Date getUpdated() {
+ return this.updated;
+ }
+
+ /**
+ * Used for testing purposes only!
+ *
+ * @param date
+ */
+ void setUpdated(Date date) {
+ this.updated = date;
+ }
+
+ /**
+ * @return the state
+ */
+ public State getState() {
+ return this.state;
+ }
+
+ /**
+ * @return the stateMsg
+ */
+ public String getStateMsg() {
+ return this.stateMsg;
+ }
+
+ /**
+ * @param state
+ * the state
+ * @param stateMsg
+ * the stateMsg
+ */
+ public void setState(State state, String stateMsg) {
+ this.state = state;
+ this.stateMsg = stateMsg;
+ this.updated = new Date();
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getParam(String key) {
+ return (T) this.parameters.get(key);
+ }
+
+ /**
+ * Add a transient parameter to this message
+ *
+ * @param key
+ * the key under which the parameter is to be stored
+ * @param value
+ * the value to store under the given key
+ */
+ public void addParam(String key, Object value) {
+ this.parameters.put(key, value);
+ }
+
+ /**
+ * Removes the parameter with the given key
+ *
+ * @param key
+ * The give of the parameter to be removed
+ * @return the removed value, or null if the object didn't exist
+ */
+ @SuppressWarnings("unchecked")
+ public T removeParam(String key) {
+ return (T) this.parameters.remove(key);
+ }
+
+ /**
+ * @return the set of parameter keys
+ */
+ public Set getParamKeys() {
+ return this.parameters.keySet();
+ }
+
+ @SuppressWarnings("nls")
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(this.getClass().getSimpleName() + " [id=");
+ builder.append(this.id);
+ builder.append(", key=");
+ builder.append(this.key);
+ builder.append(", updated=");
+ builder.append(ISO8601FormatFactory.getInstance().formatDate(this.updated));
+ builder.append(", state=");
+ builder.append(this.state);
+ if (StringHelper.isNotEmpty(this.stateMsg)) {
+ builder.append(", stateMsg=");
+ builder.append(this.stateMsg);
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ IoMessage other = (IoMessage) obj;
+ if (this.id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!this.id.equals(other.id))
+ return false;
+ return true;
+ }
+
+ public enum State {
+ CREATED, // new
+ PENDING, // outbound
+ ACCEPTED, // inbound
+ DONE, // completed
+ FAILED, // completed
+ FATAL; // not sendable
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageArchive.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageArchive.java
new file mode 100644
index 000000000..ee0af8077
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageArchive.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.util.List;
+
+public interface IoMessageArchive {
+
+ public int getMaxSize();
+
+ public void setMaxSize(int maxSize);
+
+ public int getTrimSize();
+
+ public void setTrimSize(int trimSize);
+
+ public int size();
+
+ public List getAll();
+
+ public List getBy(String connectionId);
+
+ public List getBy(String connectionId, CommandKey key);
+
+ public void clearArchive();
+
+ public void archive(IoMessage message);
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java
new file mode 100644
index 000000000..cf7d11a54
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import ch.eitchnet.communication.IoMessage.State;
+
+/**
+ * @author Robert von Burg
+ */
+public class IoMessageStateObserver implements ConnectionObserver {
+
+ private CommandKey key;
+ private String messageId;
+ private State state;
+
+ public IoMessageStateObserver(CommandKey key, String messageId) {
+ this.key = key;
+ this.messageId = messageId;
+ }
+
+ @Override
+ public void notify(CommandKey key, IoMessage message) {
+ if (this.key.equals(key) && message.getId().equals(this.messageId)) {
+ this.state = message.getState();
+
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ }
+
+ public State getState() {
+ return this.state;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java
new file mode 100644
index 000000000..b5a08f9da
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.console.ConsoleMessageVisitor;
+
+/**
+ *
+ * Visitors to read and write {@link IoMessage} using different kind of endpoints. Different endpoints will require
+ * different ways of writing or reading message, thus this is not defined here. Known extensions are
+ * {@link ConsoleMessageVisitor}, {@link StreamMessageVisitor}.
+ *
+ *
+ *
+ * Concrete implementations must be thread safe!
+ *
+ *
+ * @author Robert von Burg
+ */
+public abstract class IoMessageVisitor {
+
+ protected static final Logger logger = LoggerFactory.getLogger(IoMessageVisitor.class);
+
+ protected CommunicationConnection connection;
+
+ public void configure(CommunicationConnection connection) {
+ this.connection = connection;
+ }
+
+ public void simulate(IoMessage ioMessage) {
+ // allow for subclasses to implement
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java
new file mode 100644
index 000000000..00ad303b6
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+
+public class SimpleMessageArchive implements IoMessageArchive {
+
+ private int maxSize;
+ private int trimSize;
+ private TreeSet messageArchive;
+
+ public SimpleMessageArchive() {
+ this(1000, 100);
+ }
+
+ public SimpleMessageArchive(int maxSize, int trimSize) {
+ this.maxSize = maxSize;
+ this.trimSize = trimSize;
+
+ this.messageArchive = new TreeSet<>(new Comparator() {
+ @Override
+ public int compare(IoMessage o1, IoMessage o2) {
+ return o1.getUpdated().compareTo(o2.getUpdated());
+ }
+ });
+ }
+
+ @Override
+ public synchronized int getMaxSize() {
+ return this.maxSize;
+ }
+
+ @Override
+ public synchronized void setMaxSize(int maxSize) {
+ this.maxSize = maxSize;
+ trim();
+ }
+
+ @Override
+ public synchronized int getTrimSize() {
+ return this.trimSize;
+ }
+
+ @Override
+ public synchronized void setTrimSize(int trimSize) {
+ this.trimSize = trimSize;
+ }
+
+ @Override
+ public synchronized int size() {
+ return this.messageArchive.size();
+ }
+
+ @Override
+ public synchronized List getAll() {
+ List all = new ArrayList<>(this.messageArchive);
+ return all;
+ }
+
+ @Override
+ public synchronized List getBy(String connectionId) {
+ List all = new ArrayList<>();
+ for (IoMessage msg : this.messageArchive) {
+ if (msg.getConnectionId().equals(connectionId))
+ all.add(msg);
+ }
+ return all;
+ }
+
+ @Override
+ public synchronized List getBy(String connectionId, CommandKey key) {
+ List all = new ArrayList<>();
+ for (IoMessage msg : this.messageArchive) {
+ if (msg.getConnectionId().equals(connectionId) && msg.getKey().equals(key))
+ all.add(msg);
+ }
+ return all;
+ }
+
+ @Override
+ public synchronized void clearArchive() {
+ this.messageArchive.clear();
+ }
+
+ @Override
+ public synchronized void archive(IoMessage message) {
+ this.messageArchive.add(message);
+ trim();
+ }
+
+ protected void trim() {
+ if (this.messageArchive.size() <= this.maxSize)
+ return;
+
+ Iterator iter = this.messageArchive.iterator();
+ for (int i = 0; i <= this.trimSize; i++) {
+ if (iter.hasNext()) {
+ iter.next();
+ iter.remove();
+ }
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java
new file mode 100644
index 000000000..894ebf883
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ *
+ * {@link IoMessageVisitor} to read or write using IO Streams.
+ *
+ *
+ *
+ * Concrete implementations must be thread safe!
+ *
+ *
+ * @author Robert von Burg
+ */
+public abstract class StreamMessageVisitor extends IoMessageVisitor {
+
+ public abstract void visit(OutputStream outputStream, IoMessage message) throws Exception;
+
+ public abstract IoMessage visit(InputStream inputStream) throws Exception;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/Chat.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/Chat.java
new file mode 100644
index 000000000..5dd90d451
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/Chat.java
@@ -0,0 +1,133 @@
+package ch.eitchnet.communication.chat;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+
+import ch.eitchnet.utils.helper.StringHelper;
+
+public class Chat {
+
+ public static void main(String[] args) {
+
+ if (args.length < 4)
+ printIllegalArgsAndExit(args);
+
+ if (args[0].equals("server")) { //$NON-NLS-1$
+ startServer(args);
+ } else if (args[0].equals("client")) { //$NON-NLS-1$
+ startClient(args);
+ }
+ }
+
+ private static void startServer(String[] args) {
+
+ // server
+ InetAddress host;
+ String hostS = args[1];
+ try {
+ host = InetAddress.getByName(hostS);
+ } catch (UnknownHostException e1) {
+ System.err.println(MessageFormat.format("Illegal server address: {0}", hostS)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+ boolean isHostAddress = false;
+ try {
+ for (Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); interfaces
+ .hasMoreElements();) {
+ NetworkInterface iface = interfaces.nextElement();
+ for (Enumeration addresses = iface.getInetAddresses(); addresses.hasMoreElements();) {
+ InetAddress inetAddress = addresses.nextElement();
+ if (inetAddress.getHostAddress().equals(host.getHostAddress()))
+ isHostAddress = true;
+ }
+ }
+ } catch (SocketException e) {
+ System.err.println("Oops: " + e.getMessage()); //$NON-NLS-1$
+ }
+
+ if (!isHostAddress) {
+ System.err.println(MessageFormat.format("The address {0} is not an address of this host!", hostS)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ }
+
+ // port
+ int port;
+ String portS = args[2];
+ try {
+ port = Integer.parseInt(portS);
+ } catch (NumberFormatException e) {
+ System.err.println(MessageFormat.format("Illegal port: {0}", portS)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+ if (port < 1 || port > 65535) {
+ System.err.println(MessageFormat.format("Illegal port: {0}", port)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+
+ // username
+ String username = args[3];
+
+ // start
+ ChatServer chatServer = new ChatServer(host, port, username);
+ chatServer.start();
+ }
+
+ private static void startClient(String[] args) {
+
+ // server
+ InetAddress host;
+ String hostS = args[1];
+ try {
+ host = InetAddress.getByName(hostS);
+ } catch (UnknownHostException e1) {
+ System.err.println(MessageFormat.format("Illegal server address: {0}", hostS)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+
+ // port
+ int port;
+ String portS = args[2];
+ try {
+ port = Integer.parseInt(portS);
+ } catch (NumberFormatException e) {
+ System.err.println(MessageFormat.format("Illegal port: {0}", portS)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+ if (port < 1 || port > 65535) {
+ System.err.println(MessageFormat.format("Illegal port: {0}", port)); //$NON-NLS-1$
+ printIllegalArgsAndExit(args);
+ return;
+ }
+
+ // username
+ String username = args[3];
+
+ // start
+ ChatClient chatClient = new ChatClient(host, port, username);
+ chatClient.start();
+ }
+
+ private static void printIllegalArgsAndExit(String[] args) {
+ System.err.print("Illegal arguments: "); //$NON-NLS-1$
+ if (args.length == 0) {
+ System.err.print("(none)"); //$NON-NLS-1$
+ } else {
+ for (String arg : args) {
+ System.err.print(arg + StringHelper.SPACE);
+ }
+ }
+ System.err.println();
+ System.err.println("Usage: java ...Chat server "); //$NON-NLS-1$
+ System.err.println("Usage: java ...Chat client "); //$NON-NLS-1$
+ System.exit(1);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatClient.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatClient.java
new file mode 100644
index 000000000..5f0807281
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatClient.java
@@ -0,0 +1,92 @@
+package ch.eitchnet.communication.chat;
+
+import static ch.eitchnet.communication.chat.ChatMessageVisitor.inboundKey;
+import static ch.eitchnet.communication.chat.ChatMessageVisitor.outboundKey;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+import ch.eitchnet.communication.CommandKey;
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.ConnectionMode;
+import ch.eitchnet.communication.ConnectionObserver;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.ConnectionStateObserver;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.tcpip.ClientSocketEndpoint;
+import ch.eitchnet.communication.tcpip.SocketEndpointConstants;
+import ch.eitchnet.communication.tcpip.SocketMessageVisitor;
+
+public class ChatClient implements ConnectionObserver, ConnectionStateObserver {
+
+ private static final String id = "ChatClient"; //$NON-NLS-1$
+ private InetAddress host;
+ private int port;
+ private String username;
+ private boolean connected;
+
+ public ChatClient(InetAddress host, int port, String username) {
+ this.host = host;
+ this.port = port;
+ this.username = username;
+ }
+
+ public void start() {
+ ConnectionMode mode = ConnectionMode.ON;
+
+ Map parameters = new HashMap<>();
+ parameters.put(SocketEndpointConstants.PARAMETER_RETRY, "10000"); //$NON-NLS-1$
+ parameters.put(SocketEndpointConstants.PARAMETER_USE_TIMEOUT, Boolean.FALSE.toString());
+ parameters.put(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS, this.host.getHostAddress());
+ parameters.put(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT, Integer.toString(this.port));
+ parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.FALSE.toString());
+
+ SocketMessageVisitor messageVisitor = new ChatMessageVisitor(id);
+ ClientSocketEndpoint endpoint = new ClientSocketEndpoint();
+
+ CommunicationConnection connection = new CommunicationConnection(id, mode, parameters, endpoint, messageVisitor);
+ connection.addConnectionObserver(outboundKey, this);
+ connection.addConnectionObserver(inboundKey, this);
+ connection.addConnectionStateObserver(this);
+ connection.configure();
+ connection.start();
+
+ while (!this.connected) {
+ synchronized (this) {
+ try {
+ this.wait(2000l);
+ } catch (InterruptedException e) {
+ System.err.println("oops: " + e.getMessage()); //$NON-NLS-1$
+ }
+ }
+ }
+
+ System.out.println("Connected. Send messages to user:"); //$NON-NLS-1$
+ while (true) {
+ @SuppressWarnings("resource")
+ Scanner in = new Scanner(System.in);
+ //System.out.print(this.username + ": ");
+ String line = in.nextLine();
+ connection.send(ChatIoMessage.msg(outboundKey, id, this.username, line));
+ }
+ }
+
+ @Override
+ public void notify(CommandKey key, IoMessage message) {
+ if (key.equals(inboundKey)) {
+ ChatIoMessage chatIoMessage = (ChatIoMessage) message;
+ System.out.println(chatIoMessage.getChatMsg());
+ }
+ }
+
+ @Override
+ public void notify(ConnectionState oldState, String oldStateMsg, ConnectionState newState, String newStateMsg) {
+ this.connected = newState == ConnectionState.CONNECTED || newState == ConnectionState.IDLE
+ || newState == ConnectionState.WORKING;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java
new file mode 100644
index 000000000..a9bf851eb
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java
@@ -0,0 +1,31 @@
+package ch.eitchnet.communication.chat;
+
+import ch.eitchnet.communication.CommandKey;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.utils.helper.StringHelper;
+
+public class ChatIoMessage extends IoMessage {
+
+ private String chatMsg;
+
+ public ChatIoMessage(String id, CommandKey key, String connectionId) {
+ super(id, key, connectionId);
+ }
+
+ public String getChatMsg() {
+ return this.chatMsg;
+ }
+
+ public void setChatMsg(String chagMsg) {
+ this.chatMsg = chagMsg;
+ }
+
+ public static ChatIoMessage msg(CommandKey key, String connId, String username, String msg) {
+
+ String line = username + StringHelper.COLON + StringHelper.SPACE + msg;
+
+ ChatIoMessage chatIoMessage = new ChatIoMessage(StringHelper.getUniqueId(), key, connId);
+ chatIoMessage.setChatMsg(line);
+ return chatIoMessage;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java
new file mode 100644
index 000000000..29667480f
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java
@@ -0,0 +1,45 @@
+package ch.eitchnet.communication.chat;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.InputStreamReader;
+
+import ch.eitchnet.communication.CommandKey;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.tcpip.SocketMessageVisitor;
+import ch.eitchnet.utils.helper.StringHelper;
+
+public class ChatMessageVisitor extends SocketMessageVisitor {
+
+ public static final CommandKey inboundKey = CommandKey.key("server", "msg"); //$NON-NLS-1$//$NON-NLS-2$
+ public static final CommandKey outboundKey = CommandKey.key("client", "msg"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ public ChatMessageVisitor(String connectionId) {
+ super(connectionId);
+ }
+
+ @Override
+ public IoMessage visit(DataInputStream inputStream, DataOutputStream outputStream) throws Exception {
+
+ @SuppressWarnings("resource")
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ String line = bufferedReader.readLine();
+ if (line == null) {
+ bufferedReader.close();
+ return null;
+ }
+
+ ChatIoMessage chatIoMessage = new ChatIoMessage(StringHelper.getUniqueId(), inboundKey, this.connectionId);
+ chatIoMessage.setChatMsg(line);
+ return chatIoMessage;
+ }
+
+ @Override
+ public void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message) throws Exception {
+ ChatIoMessage chatIoMessage = (ChatIoMessage) message;
+ outputStream.writeChars(chatIoMessage.getChatMsg());
+ outputStream.writeChar('\n');
+ outputStream.flush();
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatServer.java
new file mode 100644
index 000000000..fbd9b3bde
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatServer.java
@@ -0,0 +1,93 @@
+package ch.eitchnet.communication.chat;
+
+import static ch.eitchnet.communication.chat.ChatMessageVisitor.inboundKey;
+import static ch.eitchnet.communication.chat.ChatMessageVisitor.outboundKey;
+
+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+
+import ch.eitchnet.communication.CommandKey;
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.ConnectionMode;
+import ch.eitchnet.communication.ConnectionObserver;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.ConnectionStateObserver;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.tcpip.ServerSocketEndpoint;
+import ch.eitchnet.communication.tcpip.SocketEndpointConstants;
+import ch.eitchnet.communication.tcpip.SocketMessageVisitor;
+
+public class ChatServer implements ConnectionObserver, ConnectionStateObserver {
+
+ private static final String id = "ChatServer"; //$NON-NLS-1$
+ private InetAddress address;
+ private int port;
+ private String username;
+ private boolean connected;
+
+ public ChatServer(InetAddress address, int port, String username) {
+ this.address = address;
+ this.port = port;
+ this.username = username;
+ }
+
+ public void start() {
+ ConnectionMode mode = ConnectionMode.ON;
+
+ Map parameters = new HashMap<>();
+ parameters.put(SocketEndpointConstants.PARAMETER_RETRY, "10000"); //$NON-NLS-1$
+ parameters.put(SocketEndpointConstants.PARAMETER_USE_TIMEOUT, Boolean.FALSE.toString());
+ parameters.put(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS, this.address.getHostAddress());
+ parameters.put(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT, Integer.toString(this.port));
+ parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.FALSE.toString());
+ parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.FALSE.toString());
+
+ SocketMessageVisitor messageVisitor = new ChatMessageVisitor(id);
+ ServerSocketEndpoint endpoint = new ServerSocketEndpoint();
+
+ CommunicationConnection connection = new CommunicationConnection(id, mode, parameters, endpoint, messageVisitor);
+ connection.addConnectionObserver(outboundKey, this);
+ connection.addConnectionObserver(inboundKey, this);
+ connection.addConnectionStateObserver(this);
+ connection.configure();
+ connection.start();
+
+ while (!this.connected) {
+ synchronized (this) {
+ try {
+ this.wait(2000l);
+ } catch (InterruptedException e) {
+ System.err.println("oops: " + e.getMessage()); //$NON-NLS-1$
+ }
+ }
+ }
+
+ System.out.println("Connected. Send messages to user:"); //$NON-NLS-1$
+ while (true) {
+ @SuppressWarnings("resource")
+ Scanner in = new Scanner(System.in);
+ //System.out.print(this.username + ": ");
+ String line = in.nextLine();
+ connection.send(ChatIoMessage.msg(outboundKey, id, this.username, line));
+ }
+ }
+
+ @Override
+ public void notify(CommandKey key, IoMessage message) {
+ if (key.equals(inboundKey)) {
+ ChatIoMessage chatIoMessage = (ChatIoMessage) message;
+ System.out.println(chatIoMessage.getChatMsg());
+ }
+ }
+
+ @Override
+ public void notify(ConnectionState oldState, String oldStateMsg, ConnectionState newState, String newStateMsg) {
+ this.connected = newState == ConnectionState.CONNECTED || newState == ConnectionState.IDLE
+ || newState == ConnectionState.WORKING;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java
new file mode 100644
index 000000000..26bcf14e8
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.console;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.CommunicationEndpoint;
+import ch.eitchnet.communication.ConnectionMessages;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessageVisitor;
+
+/**
+ * @author Robert von Burg
+ */
+public class ConsoleEndpoint implements CommunicationEndpoint {
+
+ private static final Logger logger = LoggerFactory.getLogger(ConsoleEndpoint.class);
+ private CommunicationConnection connection;
+ private ConsoleMessageVisitor messageVisitor;
+
+ @Override
+ public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) {
+ this.connection = connection;
+ ConnectionMessages.assertLegalMessageVisitor(this.getClass(), ConsoleMessageVisitor.class, messageVisitor);
+ this.messageVisitor = (ConsoleMessageVisitor) messageVisitor;
+ }
+
+ @Override
+ public void start() {
+ this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString());
+ }
+
+ @Override
+ public void stop() {
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTED.toString());
+ }
+
+ @Override
+ public void reset() {
+ this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
+ }
+
+ @Override
+ public String getLocalUri() {
+ return "console"; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getRemoteUri() {
+ return "console"; //$NON-NLS-1$
+ }
+
+ @Override
+ public void send(IoMessage message) throws Exception {
+ this.connection.notifyStateChange(ConnectionState.WORKING, ConnectionState.WORKING.toString());
+ try {
+ this.messageVisitor.visit(logger, message);
+ } finally {
+ this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString());
+ }
+ }
+
+ @Override
+ public void simulate(IoMessage message) throws Exception {
+ send(message);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java
new file mode 100644
index 000000000..ccad0ef30
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.console;
+
+import org.slf4j.Logger;
+
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessageVisitor;
+
+public abstract class ConsoleMessageVisitor extends IoMessageVisitor {
+
+ public abstract void visit(Logger logger, IoMessage message) throws Exception;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java
new file mode 100644
index 000000000..459bf36ab
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.file;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.CommunicationEndpoint;
+import ch.eitchnet.communication.ConnectionException;
+import ch.eitchnet.communication.ConnectionMessages;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessageVisitor;
+import ch.eitchnet.communication.StreamMessageVisitor;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * An {@link CommunicationEndpoint} which writes and/or reads from a designated file
+ *
+ * @author Robert von Burg
+ */
+public class FileEndpoint implements CommunicationEndpoint, Runnable {
+
+ public static final String ENDPOINT_MODE = "endpointMode"; //$NON-NLS-1$
+ public static final String INBOUND_FILENAME = "inboundFilename"; //$NON-NLS-1$
+ public static final String OUTBOUND_FILENAME = "outboundFilename"; //$NON-NLS-1$
+ public static final long POLL_TIME = 1000l;
+
+ private static final Logger logger = LoggerFactory.getLogger(FileEndpoint.class);
+
+ private CommunicationConnection connection;
+
+ private FileEndpointMode endpointMode;
+ private String inboundFilename;
+ private String outboundFilename;
+ private Thread thread;
+ private boolean run = false;
+ private StreamMessageVisitor messageVisitor;
+
+ /**
+ * {@link FileEndpoint} needs the following parameters on the configuration to be initialized
+ *
+ *
outboundFilename: the file name where the {@link IoMessage} contents are written to. The value may contain
+ * {@link System#getProperty(String)} place holders which will be evaluated
+ *
+ */
+ @Override
+ public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) {
+ this.connection = connection;
+
+ ConnectionMessages.assertLegalMessageVisitor(this.getClass(), StreamMessageVisitor.class, messageVisitor);
+ this.messageVisitor = (StreamMessageVisitor) messageVisitor;
+
+ configure();
+ }
+
+ private void configure() {
+ Map parameters = this.connection.getParameters();
+
+ String endpointModeS = parameters.get(ENDPOINT_MODE);
+ if (StringHelper.isEmpty(endpointModeS)) {
+ throw ConnectionMessages.throwInvalidParameter(FileEndpoint.class, ENDPOINT_MODE, endpointModeS);
+ }
+ try {
+ this.endpointMode = FileEndpointMode.valueOf(endpointModeS);
+ } catch (Exception e) {
+ throw ConnectionMessages.throwInvalidParameter(FileEndpoint.class, ENDPOINT_MODE, endpointModeS);
+ }
+
+ if (this.endpointMode.isRead()) {
+ this.inboundFilename = parameters.get(INBOUND_FILENAME);
+ if (StringHelper.isEmpty(this.inboundFilename)) {
+ throw ConnectionMessages.throwInvalidParameter(FileEndpoint.class, INBOUND_FILENAME,
+ this.inboundFilename);
+ }
+ }
+
+ if (this.endpointMode.isWrite()) {
+ this.outboundFilename = parameters.get(OUTBOUND_FILENAME);
+ if (StringHelper.isEmpty(this.outboundFilename)) {
+ throw ConnectionMessages.throwInvalidParameter(FileEndpoint.class, OUTBOUND_FILENAME,
+ this.outboundFilename);
+ }
+ }
+ }
+
+ @Override
+ public String getLocalUri() {
+ return new File(this.inboundFilename).getAbsolutePath();
+ }
+
+ @Override
+ public String getRemoteUri() {
+ return new File(this.outboundFilename).getAbsolutePath();
+ }
+
+ @Override
+ public void start() {
+ if (this.endpointMode.isRead()) {
+ this.thread = new Thread(this, new File(this.inboundFilename).getName());
+ this.run = true;
+ this.thread.start();
+ }
+ this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString());
+ }
+
+ @Override
+ public void stop() {
+ stopThread();
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTED.toString());
+ }
+
+ @Override
+ public void reset() {
+ stopThread();
+ configure();
+ this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
+ }
+
+ private void stopThread() {
+ this.run = false;
+ if (this.thread != null) {
+ try {
+ this.thread.interrupt();
+ this.thread.join(2000l);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Error while interrupting thread: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ }
+
+ this.thread = null;
+ }
+ }
+
+ @Override
+ public void send(IoMessage message) throws Exception {
+ if (!this.endpointMode.isWrite()) {
+ String msg = "FileEnpoint mode is {0} and thus write is not allowed!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, this.endpointMode);
+ throw new ConnectionException(msg);
+ }
+
+ this.connection.notifyStateChange(ConnectionState.WORKING, ConnectionState.WORKING.toString());
+
+ // open the stream
+ try (FileOutputStream outputStream = new FileOutputStream(this.outboundFilename, false)) {
+
+ // write the message using the visitor
+ this.messageVisitor.visit(outputStream, message);
+
+ } finally {
+ this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString());
+ }
+ }
+
+ @Override
+ public void simulate(IoMessage message) throws Exception {
+ this.messageVisitor.simulate(message);
+ }
+
+ @Override
+ public void run() {
+
+ File file = new File(this.inboundFilename);
+
+ long lastModified = 0l;
+
+ logger.info("Starting..."); //$NON-NLS-1$
+ while (this.run) {
+
+ try {
+
+ if (file.canRead()) {
+ long tmpModified = file.lastModified();
+ if (tmpModified > lastModified) {
+
+ logger.info(MessageFormat.format("Handling file {0}", file.getAbsolutePath())); //$NON-NLS-1$
+
+ this.connection.notifyStateChange(ConnectionState.WORKING, ConnectionState.WORKING.toString());
+
+ // file is changed
+ lastModified = tmpModified;
+
+ // read the file
+ handleFile(file);
+
+ this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString());
+ }
+ }
+
+ if (this.run) {
+ this.connection.notifyStateChange(ConnectionState.WAITING, ConnectionState.WAITING.toString());
+ try {
+ synchronized (this) {
+ this.wait(POLL_TIME);
+ }
+ } catch (InterruptedException e) {
+ this.run = false;
+ logger.info("Interrupted!"); //$NON-NLS-1$
+ }
+ }
+
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Error reading file: {0}", file.getAbsolutePath())); //$NON-NLS-1$
+ logger.error(e.getMessage(), e);
+
+ this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
+ }
+ }
+ }
+
+ /**
+ * Reads the file and handle using {@link StreamMessageVisitor}
+ *
+ * @param file
+ * the {@link File} to read
+ */
+ protected void handleFile(File file) throws Exception {
+
+ try (InputStream inputStream = new FileInputStream(file)) {
+
+ // convert the object to an integration message
+ IoMessage message = this.messageVisitor.visit(inputStream);
+
+ // and forward to the connection
+ if (message != null) {
+ this.connection.handleNewMessage(message);
+ }
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java
new file mode 100644
index 000000000..842b0b00e
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.file;
+
+public enum FileEndpointMode {
+ READ(true, false), WRITE(false, true), READ_WRITE(true, true);
+ private boolean read;
+ private boolean write;
+
+ private FileEndpointMode(boolean read, boolean write) {
+ this.read = read;
+ this.write = write;
+ }
+
+ public boolean isRead() {
+ return this.read;
+ }
+
+ public boolean isWrite() {
+ return this.write;
+ }
+}
\ No newline at end of file
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java
new file mode 100644
index 000000000..7ea688be1
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.tcpip;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.text.MessageFormat;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.CommunicationEndpoint;
+import ch.eitchnet.communication.ConnectionException;
+import ch.eitchnet.communication.ConnectionMessages;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessage.State;
+import ch.eitchnet.communication.IoMessageVisitor;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ *
+ * This {@link CommunicationEndpoint} is an abstract implementation with everything needed to connect through a
+ * {@link Socket} to a remote server which is listening for incoming {@link Socket} connections. This {@link Socket}
+ * endpoint can send messages to the remote side, as well as receive messages from the remote side
+ *
+ *
+ * This endpoint is maintained as a client connection. This means that this endpoint opens the {@link Socket} to the
+ * remote server
+ *
+ *
+ * @author Robert von Burg
+ */
+public class ClientSocketEndpoint implements CommunicationEndpoint {
+
+ protected static final Logger logger = LoggerFactory.getLogger(ClientSocketEndpoint.class);
+
+ // state variables
+ private boolean connected;
+ private boolean closed;
+ private long lastConnect;
+ private boolean useTimeout;
+ private int timeout;
+ private long retry;
+ private boolean clearOnConnect;
+ private boolean connectOnStart;
+ private boolean closeAfterSend;
+
+ // remote address
+ private String remoteInputAddressS;
+ private int remoteInputPort;
+ private String localOutputAddressS;
+ private int localOutputPort;
+
+ private InetAddress remoteInputAddress;
+ private InetAddress localOutputAddress;
+
+ // connection
+ private Socket socket;
+
+ protected DataOutputStream outputStream;
+ protected DataInputStream inputStream;
+
+ protected CommunicationConnection connection;
+
+ protected SocketMessageVisitor messageVisitor;
+
+ /**
+ * Default constructor
+ */
+ public ClientSocketEndpoint() {
+ this.connected = false;
+ this.closed = true;
+ }
+
+ /**
+ * Checks the state of the connection and returns true if {@link Socket} is connected and ready for transmission,
+ * false otherwise
+ *
+ * @return true if {@link Socket} is connected and ready for transmission, false otherwise
+ */
+ protected boolean checkConnection() {
+ return !this.closed
+ && this.connected
+ && (this.socket != null && !this.socket.isClosed() && this.socket.isBound()
+ && this.socket.isConnected() && !this.socket.isInputShutdown() && !this.socket
+ .isOutputShutdown());
+ }
+
+ /**
+ * Establishes a {@link Socket} connection to the remote server. This method blocks till a connection is
+ * established. In the event of a connection failure, the method waits a configured time before retrying
+ */
+ protected void openConnection() {
+
+ ConnectionState state = this.connection.getState();
+
+ // do not allow connecting if state is
+ // - CREATED
+ // - CONNECTING
+ // - WAITING
+ // - CLOSED
+ if (state == ConnectionState.CREATED || state == ConnectionState.CONNECTING || state == ConnectionState.WAITING
+ || state == ConnectionState.DISCONNECTED) {
+
+ ConnectionMessages.throwIllegalConnectionState(state, ConnectionState.CONNECTING);
+ }
+
+ // first close the connection
+ closeConnection();
+
+ while (!this.connected && !this.closed) {
+ try {
+
+ this.connection.notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
+
+ // only try in proper intervals
+ long currentTime = System.currentTimeMillis();
+ long timeDifference = currentTime - this.lastConnect;
+ if (timeDifference < this.retry) {
+ long wait = (this.retry - timeDifference);
+ logger.info(MessageFormat.format("Waiting: {0}ms", wait)); //$NON-NLS-1$
+
+ this.connection.notifyStateChange(ConnectionState.WAITING, ConnectionState.WAITING.toString());
+ Thread.sleep(wait);
+ this.connection
+ .notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
+ }
+
+ // don't try and connect if we are closed!
+ if (this.closed) {
+ logger.error("The connection has been closed and can not be connected"); //$NON-NLS-1$
+ closeConnection();
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ return;
+ }
+
+ // keep track of the time of this connection attempt
+ this.lastConnect = System.currentTimeMillis();
+
+ // open the socket
+ if (this.localOutputAddress != null) {
+ String msg = "Opening connection to {0}:{1} from {2}:{3}..."; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.remoteInputAddress.getHostAddress(),
+ Integer.toString(this.remoteInputPort), this.localOutputAddress.getHostAddress(),
+ Integer.toString(this.localOutputPort)));
+ this.socket = new Socket(this.remoteInputAddress, this.remoteInputPort, this.localOutputAddress,
+ this.localOutputPort);
+ } else {
+ String msg = "Opening connection to {0}:{1}..."; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.remoteInputAddress.getHostAddress(),
+ Integer.toString(this.remoteInputPort)));
+ this.socket = new Socket(this.remoteInputAddress, this.remoteInputPort);
+ }
+
+ // configure the socket
+ if (logger.isDebugEnabled()) {
+ String msg = "BufferSize (send/read): {0} / {1} SoLinger: {2} TcpNoDelay: {3}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.socket.getSendBufferSize(),
+ this.socket.getReceiveBufferSize(), this.socket.getSoLinger(), this.socket.getTcpNoDelay()));
+ }
+ //outputSocket.setSendBufferSize(1);
+ //outputSocket.setSoLinger(true, 0);
+ //outputSocket.setTcpNoDelay(true);
+
+ // activate connection timeout
+ if (this.useTimeout) {
+ this.socket.setSoTimeout(this.timeout);
+ }
+
+ // get the streams
+ this.outputStream = new DataOutputStream(this.socket.getOutputStream());
+ this.inputStream = new DataInputStream(this.socket.getInputStream());
+
+ if (this.clearOnConnect) {
+ // clear the input stream
+ int available = this.inputStream.available();
+ logger.info(MessageFormat.format("clearOnConnect: skipping {0} bytes.", available)); //$NON-NLS-1$
+ this.inputStream.skip(available);
+ }
+
+ String msg = "Connected {0}: {1}:{2} with local side {3}:{4}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.connection.getId(), this.remoteInputAddressS,
+ Integer.toString(this.remoteInputPort), this.socket.getLocalAddress().getHostAddress(),
+ Integer.toString(this.socket.getLocalPort())));
+
+ // we are connected!
+ this.connection.notifyStateChange(ConnectionState.CONNECTED, ConnectionState.CONNECTED.toString());
+ this.connected = true;
+
+ } catch (InterruptedException e) {
+ logger.info("Interrupted!"); //$NON-NLS-1$
+ this.closed = true;
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ } catch (Exception e) {
+ String msg = "Error while connecting to {0}:{1}: {2}"; //$NON-NLS-1$
+ logger.error(
+ MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort)),
+ e.getMessage());
+ this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
+ }
+ }
+ }
+
+ /**
+ * closes the connection HARD by calling close() on the streams and socket. All Exceptions are caught to make sure
+ * that the connections are cleaned up
+ */
+ protected void closeConnection() {
+
+ this.connected = false;
+ this.connection.notifyStateChange(ConnectionState.BROKEN, null);
+
+ if (this.outputStream != null) {
+ try {
+ this.outputStream.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing OutputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.outputStream = null;
+ }
+ }
+
+ if (this.inputStream != null) {
+ try {
+ this.inputStream.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing InputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.inputStream = null;
+ }
+ }
+
+ if (this.socket != null) {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing OutputSocket: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.socket = null;
+ }
+
+ String msg = "Socket closed for connection {0} at remote input address {1}:{2}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.connection.getId(), this.remoteInputAddressS,
+ Integer.toString(this.remoteInputPort)));
+ }
+ }
+
+ /**
+ *
+ * Configures this {@link ClientSocketEndpoint}
+ *
+ * gets the parameter map from the connection and reads the following parameters from the map:
+ *
+ *
remoteInputAddress - the IP or Hostname of the remote server
+ *
remoteInputPort - the port to which the socket should be established
+ *
localOutputAddress - the IP or Hostname of the local server (if null, then the network layer will decide)
+ *
localOutputPort - the local port from which the socket should go out of (if null, then the network layer will
+ * decide)
+ *
retry - a configured retry wait time. Default is {@link SocketEndpointConstants#RETRY}
+ *
timeout - the timeout after which an idle socket is deemed dead. Default is
+ * {@link SocketEndpointConstants#TIMEOUT}
+ *
useTimeout - if true, then the timeout is activated, otherwise it is. default is
+ * {@link SocketEndpointConstants#USE_TIMEOUT}
+ *
clearOnConnect - if true, then the after a successful connect the input is cleared by discarding all
+ * available bytes. This can be useful in cases where the channel is clogged with stale data. default is
+ * {@link SocketEndpointConstants#CLEAR_ON_CONNECT}
+ *
connectOnStart - if true, then when the connection is started, the connection to the remote address is
+ * attempted. default is {@link SocketEndpointConstants#CONNECT_ON_START}
+ *
+ *
+ * @see CommunicationEndpoint#configure(CommunicationConnection, IoMessageVisitor)
+ */
+ @Override
+ public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) {
+ if (this.connection != null && connection.getState().compareTo(ConnectionState.INITIALIZED) > 0) {
+ String msg = "{0}:{1} already configured."; //$NON-NLS-1$
+ logger.warn(MessageFormat.format(msg, this.getClass().getSimpleName(), connection.getId()));
+ return;
+ }
+
+ ConnectionMessages.assertLegalMessageVisitor(this.getClass(), SocketMessageVisitor.class, messageVisitor);
+ this.messageVisitor = (SocketMessageVisitor) messageVisitor;
+ this.connection = connection;
+ configure();
+ }
+
+ private void configure() {
+ Map parameters = this.connection.getParameters();
+
+ this.remoteInputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS);
+ String remoteInputPortS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT);
+ this.localOutputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_ADDRESS);
+ String localOutputPortS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_PORT);
+
+ // parse remote input Address to InetAddress object
+ try {
+ this.remoteInputAddress = InetAddress.getByName(this.remoteInputAddressS);
+ } catch (UnknownHostException e) {
+ throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS, this.remoteInputAddressS);
+ }
+
+ // parse remote input address port to integer
+ try {
+ this.remoteInputPort = Integer.parseInt(remoteInputPortS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT, remoteInputPortS);
+ }
+
+ // if local output address is not set, then we will use the localhost InetAddress
+ if (this.localOutputAddressS == null || this.localOutputAddressS.length() == 0) {
+ logger.debug("No localOutputAddress set. Using localhost"); //$NON-NLS-1$
+ } else {
+
+ // parse local output address name to InetAddress object
+ try {
+ this.localOutputAddress = InetAddress.getByName(this.localOutputAddressS);
+ } catch (UnknownHostException e) {
+ String msg = "The host name ''{0}'' can not be evaluated to an internet address"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, this.localOutputAddressS);
+ throw new ConnectionException(msg, e);
+ }
+
+ // parse local output address port to integer
+ try {
+ this.localOutputPort = Integer.parseInt(localOutputPortS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_LOCAL_OUTPUT_PORT, localOutputPortS);
+ }
+ }
+
+ // configure retry wait time
+ String retryS = parameters.get(SocketEndpointConstants.PARAMETER_RETRY);
+ if (retryS == null || retryS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class, SocketEndpointConstants.PARAMETER_RETRY,
+ String.valueOf(SocketEndpointConstants.RETRY));
+ this.retry = SocketEndpointConstants.RETRY;
+ } else {
+ try {
+ this.retry = Long.parseLong(retryS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_RETRY, retryS);
+ }
+ }
+
+ // configure connect on start
+ String connectOnStartS = parameters.get(SocketEndpointConstants.PARAMETER_CONNECT_ON_START);
+ if (StringHelper.isNotEmpty(connectOnStartS)) {
+ this.connectOnStart = StringHelper.parseBoolean(connectOnStartS);
+ } else {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_CONNECT_ON_START,
+ String.valueOf(SocketEndpointConstants.CONNECT_ON_START));
+ this.connectOnStart = SocketEndpointConstants.CONNECT_ON_START;
+ }
+
+ // configure closeAfterSend
+ String closeAfterSendS = parameters.get(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND);
+ if (StringHelper.isNotEmpty(closeAfterSendS)) {
+ this.closeAfterSend = StringHelper.parseBoolean(closeAfterSendS);
+ } else {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND,
+ String.valueOf(SocketEndpointConstants.CLOSE_AFTER_SEND));
+ this.closeAfterSend = SocketEndpointConstants.CLOSE_AFTER_SEND;
+ }
+
+ // configure if timeout on connection should be activated
+ String useTimeoutS = parameters.get(SocketEndpointConstants.PARAMETER_USE_TIMEOUT);
+ if (useTimeoutS == null || useTimeoutS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_USE_TIMEOUT, String.valueOf(SocketEndpointConstants.USE_TIMEOUT));
+ this.useTimeout = SocketEndpointConstants.USE_TIMEOUT;
+ } else {
+ this.useTimeout = Boolean.parseBoolean(useTimeoutS);
+ }
+
+ if (this.useTimeout) {
+ // configure timeout on connection
+ String timeoutS = parameters.get(SocketEndpointConstants.PARAMETER_TIMEOUT);
+ if (timeoutS == null || timeoutS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_TIMEOUT, String.valueOf(SocketEndpointConstants.TIMEOUT));
+ this.timeout = SocketEndpointConstants.TIMEOUT;
+ } else {
+ try {
+ this.timeout = Integer.parseInt(timeoutS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_TIMEOUT, timeoutS);
+ }
+ }
+ }
+
+ // configure if the connection should be cleared on connect
+ String clearOnConnectS = parameters.get(SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT);
+ if (clearOnConnectS == null || clearOnConnectS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ClientSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT,
+ String.valueOf(SocketEndpointConstants.CLEAR_ON_CONNECT));
+ this.clearOnConnect = SocketEndpointConstants.CLEAR_ON_CONNECT;
+ } else {
+ this.clearOnConnect = Boolean.parseBoolean(clearOnConnectS);
+ }
+ }
+
+ /**
+ * @return the uri as String to which this {@link ClientSocketEndpoint} is locally bound to
+ */
+ @Override
+ public String getLocalUri() {
+ if (this.socket != null) {
+ InetAddress localAddress = this.socket.getLocalAddress();
+ return localAddress.getHostAddress() + StringHelper.COLON + this.socket.getLocalPort();
+ } else if (this.localOutputAddress != null) {
+ return this.localOutputAddress.getHostAddress() + StringHelper.COLON + this.localOutputPort;
+ }
+
+ return "0.0.0.0:0"; //$NON-NLS-1$
+ }
+
+ /**
+ * @return the uri as String to which this {@link ClientSocketEndpoint} is connecting to
+ */
+ @Override
+ public String getRemoteUri() {
+ if (this.socket != null) {
+ InetAddress remoteAddress = this.socket.getInetAddress();
+ return remoteAddress.getHostAddress() + StringHelper.COLON + this.socket.getPort();
+ } else if (this.remoteInputAddress != null) {
+ return this.remoteInputAddress.getHostAddress() + StringHelper.COLON + this.remoteInputPort;
+ }
+
+ return this.remoteInputAddressS + StringHelper.COLON + this.remoteInputPort;
+ }
+
+ /**
+ * Allows this end point to connect and then opens the connection to the defined remote server
+ *
+ * @see CommunicationEndpoint#start()
+ */
+ @Override
+ public void start() {
+ if (!this.closed) {
+ logger.warn(MessageFormat.format("CommunicationConnection {0} already started.", this.connection.getId())); //$NON-NLS-1$
+ } else {
+ // logger.info(MessageFormat.format("Enabling connection {0}...", this.connection.getId())); //$NON-NLS-1$
+ this.closed = false;
+ this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
+ if (this.connectOnStart) {
+ openConnection();
+ }
+ }
+ }
+
+ /**
+ * Closes this connection and disallows this end point to reconnect
+ *
+ * @see CommunicationEndpoint#stop()
+ */
+ @Override
+ public void stop() {
+ this.closed = true;
+
+ closeConnection();
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTED.toString());
+
+ logger.info(MessageFormat.format("Disabled connection {0}.", this.connection.getId())); //$NON-NLS-1$
+ }
+
+ @Override
+ public void reset() {
+ this.closed = true;
+ closeConnection();
+ this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
+ }
+
+ @Override
+ public void simulate(IoMessage message) throws Exception {
+ this.messageVisitor.simulate(message);
+ }
+
+ @Override
+ public void send(IoMessage message) throws Exception {
+
+ while (!this.closed && message.getState() == State.PENDING) {
+ try {
+
+ // open the connection
+ if (!checkConnection())
+ openConnection();
+
+ // read and write to the client socket
+ this.messageVisitor.visit(this.inputStream, this.outputStream, message);
+
+ message.setState(State.DONE, State.DONE.name());
+
+ } catch (Exception e) {
+ if (this.closed) {
+ logger.warn("Socket has been closed!"); //$NON-NLS-1$
+ message.setState(State.FATAL, "Socket has been closed!"); //$NON-NLS-1$
+ } else {
+ closeConnection();
+ logger.error(e.getMessage(), e);
+ message.setState(State.FATAL, e.getLocalizedMessage());
+ this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
+ }
+ } finally {
+ if (this.closeAfterSend && !this.closed) {
+ closeConnection();
+ }
+ }
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java
new file mode 100644
index 000000000..57f6399ab
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.tcpip;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.BindException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.text.MessageFormat;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.communication.CommunicationConnection;
+import ch.eitchnet.communication.CommunicationEndpoint;
+import ch.eitchnet.communication.ConnectionException;
+import ch.eitchnet.communication.ConnectionMessages;
+import ch.eitchnet.communication.ConnectionState;
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessageVisitor;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ *
+ * This {@link CommunicationEndpoint} is an abstract implementation with everything needed to start a {@link Socket}
+ * server which waits for a request from a single client
+ *
+ *
+ * This end point only allows a single connection at a time and implements all exception handling for opening and
+ * closing a {@link Socket} connection
+ *
+ *
+ * @see ServerSocketEndpoint#configure(CommunicationConnection, IoMessageVisitor) for details on configuring the end
+ * point
+ *
+ * @author Robert von Burg
+ */
+public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable {
+
+ protected static final Logger logger = LoggerFactory.getLogger(ServerSocketEndpoint.class);
+
+ private Thread serverThread;
+
+ // state variables
+ private boolean connected;
+ private boolean closed;
+ private boolean fatal;
+ private long lastConnect;
+ private boolean useTimeout;
+ private int timeout;
+ private long retry;
+ private boolean clearOnConnect;
+
+ // address
+ private String localInputAddressS;
+ private int localInputPort;
+ private String remoteOutputAddressS;
+ private int remoteOutputPort;
+
+ private InetAddress localInputAddress;
+ private InetAddress remoteOutputAddress;
+
+ // connection
+ private ServerSocket serverSocket;
+ private Socket socket;
+
+ protected DataOutputStream outputStream;
+ protected DataInputStream inputStream;
+
+ protected CommunicationConnection connection;
+
+ protected SocketMessageVisitor messageVisitor;
+
+ /**
+ * Default constructor
+ */
+ public ServerSocketEndpoint() {
+ this.connected = false;
+ this.closed = true;
+ this.fatal = false;
+ }
+
+ /**
+ * Checks the state of the connection and returns true if {@link Socket} is connected and ready for transmission,
+ * false otherwise
+ *
+ * @return true if {@link Socket} is connected and ready for transmission, false otherwise
+ */
+ protected boolean checkConnection() {
+ return !this.closed
+ && this.connected
+ && (this.socket != null && !this.socket.isClosed() && this.socket.isBound()
+ && this.socket.isConnected() && !this.socket.isInputShutdown() && !this.socket
+ .isOutputShutdown());
+ }
+
+ /**
+ * Listens on the {@link ServerSocket} for an incoming connection. Prepares the connection then for use. If the
+ * remote address has been defined, then the remote connection is validated to come from this appropriate host.
+ * CommunicationConnection attempts are always separated by a configured amount of time
+ */
+ protected void openConnection() {
+
+ ConnectionState state = this.connection.getState();
+
+ // do not open the connection if state is
+ // - CREATED
+ // - CONNECTING
+ // - WAITING
+ // - CLOSED
+ if (state == ConnectionState.CREATED || state == ConnectionState.CONNECTING || state == ConnectionState.WAITING
+ || state == ConnectionState.DISCONNECTED) {
+
+ ConnectionMessages.throwIllegalConnectionState(state, ConnectionState.CONNECTING);
+ }
+
+ // first close the connection
+ closeConnection();
+
+ while (!this.connected && !this.closed) {
+ try {
+
+ this.connection.notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
+
+ // only try in proper intervals
+ long currentTime = System.currentTimeMillis();
+ long timeDifference = currentTime - this.lastConnect;
+ if (timeDifference < this.retry) {
+ long wait = this.retry - timeDifference;
+ logger.info(MessageFormat.format("Waiting: {0}ms", wait)); //$NON-NLS-1$
+
+ this.connection.notifyStateChange(ConnectionState.WAITING, ConnectionState.WAITING.toString());
+ Thread.sleep(wait);
+ this.connection
+ .notifyStateChange(ConnectionState.CONNECTING, ConnectionState.CONNECTING.toString());
+ }
+
+ // don't try and connect if we are closed!
+ if (this.closed) {
+ logger.error("The connection has been closed and can not be connected"); //$NON-NLS-1$
+ closeConnection();
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ return;
+ }
+
+ // keep track of the time of this connection attempt
+ this.lastConnect = System.currentTimeMillis();
+
+ // open the socket
+ String msg = "Waiting for connections on: {0}:{1}..."; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.localInputAddress.getHostAddress(),
+ Integer.toString(this.localInputPort)));
+ this.socket = this.serverSocket.accept();
+
+ // validate that the remote side of the socket is really the client we want
+ if (this.remoteOutputAddress != null) {
+
+ String remoteAddr = this.socket.getInetAddress().getHostAddress();
+ if (!remoteAddr.equals(this.remoteOutputAddress.getHostAddress())) {
+ msg = "Illegal remote client at address {0}. Expected is {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, remoteAddr, this.remoteOutputAddress.getHostAddress());
+ logger.error(msg);
+
+ closeConnection();
+
+ throw new ConnectionException(msg);
+ }
+ }
+
+ // configure the socket
+ if (logger.isDebugEnabled()) {
+ msg = "BufferSize (send/read): {0} / {1} SoLinger: {2} TcpNoDelay: {3}"; //$NON-NLS-1$
+ logger.debug(MessageFormat.format(msg, this.socket.getSendBufferSize(),
+ this.socket.getReceiveBufferSize(), this.socket.getSoLinger(), this.socket.getTcpNoDelay()));
+ }
+ //inputSocket.setSendBufferSize(1);
+ //inputSocket.setSoLinger(true, 0);
+ //inputSocket.setTcpNoDelay(true);
+
+ // activate connection timeout
+ if (this.useTimeout) {
+ this.socket.setSoTimeout(this.timeout);
+ }
+
+ // get the streams
+ this.outputStream = new DataOutputStream(this.socket.getOutputStream());
+ this.inputStream = new DataInputStream(this.socket.getInputStream());
+
+ if (this.clearOnConnect) {
+ // clear the input stream
+ int available = this.inputStream.available();
+ logger.info(MessageFormat.format("clearOnConnect: skipping {0} bytes.", available)); //$NON-NLS-1$
+ this.inputStream.skip(available);
+ }
+
+ msg = "Connected {0}{1}: {2}:{3} with local side {4}:{5}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.getClass().getSimpleName(), this.connection.getId(),
+ this.socket.getInetAddress().getHostName(), Integer.toString(this.socket.getPort()),
+ this.socket.getLocalAddress().getHostAddress(), Integer.toString(this.socket.getLocalPort())));
+
+ // we are connected!
+ this.connection.notifyStateChange(ConnectionState.CONNECTED, ConnectionState.CONNECTED.toString());
+ this.connected = true;
+
+ } catch (InterruptedException e) {
+ logger.warn("Interrupted!"); //$NON-NLS-1$
+ this.closed = true;
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ } catch (Exception e) {
+ if (this.closed && e instanceof SocketException) {
+ logger.warn("Socket closed!"); //$NON-NLS-1$
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ } else {
+ String msg = "Error while opening socket for inbound connection {0}: {1}"; //$NON-NLS-1$
+ logger.error(MessageFormat.format(msg, this.connection.getId()), e.getMessage());
+ this.connected = false;
+ this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * closes the connection HARD by calling close() on the streams and socket. All Exceptions are caught to make sure
+ * that the connections are cleaned up
+ */
+ protected void closeConnection() {
+
+ this.connected = false;
+ this.connection.notifyStateChange(ConnectionState.BROKEN, null);
+
+ if (this.outputStream != null) {
+ try {
+ this.outputStream.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing OutputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.outputStream = null;
+ }
+ }
+
+ if (this.inputStream != null) {
+ try {
+ this.inputStream.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing InputStream: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.inputStream = null;
+ }
+ }
+
+ if (this.socket != null) {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("Error closing InputSocket: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ } finally {
+ this.socket = null;
+ }
+
+ String msg = "Socket closed for inbound connection {0} at local input address {1}:{2}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, this.connection.getId(), this.localInputAddressS,
+ Integer.toString(this.localInputPort)));
+ }
+ }
+
+ /**
+ *
+ * Configures this {@link ServerSocketEndpoint}
+ *
+ * gets the parameter map from the connection and reads the following parameters from the map:
+ *
+ *
localInputAddress - the local IP or Hostname to bind to for incoming connections
+ *
localInputPort - the local port on which to listen for incoming connections
+ *
remoteOutputAddress - the IP or Hostname of the remote client. If this value is not null, then it will be
+ * verified that the connecting client is connecting from this address
+ *
remoteOutputPort - the port from which the remote client must connect. If this value is not null, then it
+ * will be verified that the connecting client is connecting from this port
+ *
retry - a configured retry wait time. Default is {@link SocketEndpointConstants#RETRY}
+ *
timeout - the timeout after which an idle socket is deemed dead. Default is
+ * {@link SocketEndpointConstants#TIMEOUT}
+ *
useTimeout - if true, then the timeout is activated. default is {@link SocketEndpointConstants#USE_TIMEOUT}
+ *
clearOnConnect - if true, then the after a successful connect the input is cleared by discarding all
+ * available bytes. This can be useful in cases where the channel is clogged with stale data. default is
+ * {@link SocketEndpointConstants#CLEAR_ON_CONNECT}
+ *
+ *
+ * @see CommunicationEndpoint#configure(CommunicationConnection, IoMessageVisitor)
+ */
+ @Override
+ public void configure(CommunicationConnection connection, IoMessageVisitor messageVisitor) {
+ if (this.connection != null && connection.getState().compareTo(ConnectionState.INITIALIZED) > 0) {
+ logger.warn(MessageFormat.format("Inbound connection {0} already configured.", connection.getId())); //$NON-NLS-1$
+ return;
+ }
+
+ ConnectionMessages.assertLegalMessageVisitor(this.getClass(), SocketMessageVisitor.class, messageVisitor);
+ this.messageVisitor = (SocketMessageVisitor) messageVisitor;
+ this.connection = connection;
+ configure();
+ }
+
+ private void configure() {
+ Map parameters = this.connection.getParameters();
+
+ this.localInputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS);
+ String localInputPortS = parameters.get(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT);
+ this.remoteOutputAddressS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_ADDRESS);
+ String remoteOutputPortS = parameters.get(SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_PORT);
+
+ // parse local Address to InetAddress object
+ try {
+ this.localInputAddress = InetAddress.getByName(this.localInputAddressS);
+ } catch (UnknownHostException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS, this.localInputAddressS);
+ }
+
+ // parse local address port to integer
+ try {
+ this.localInputPort = Integer.parseInt(localInputPortS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT, localInputPortS);
+ }
+
+ // if remote address is not set, then we will use the localhost InetAddress
+ if (this.remoteOutputAddressS == null || this.remoteOutputAddressS.length() == 0) {
+ logger.debug("No remoteOutputAddress set. Allowing connection from any remote address"); //$NON-NLS-1$
+ } else {
+
+ // parse remote output address name to InetAddress object
+ try {
+ this.remoteOutputAddress = InetAddress.getByName(this.remoteOutputAddressS);
+ } catch (UnknownHostException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_ADDRESS, this.remoteOutputAddressS);
+ }
+
+ // parse remote output address port to integer
+ try {
+ this.remoteOutputPort = Integer.parseInt(remoteOutputPortS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_REMOTE_OUTPUT_PORT, remoteOutputPortS);
+ }
+ }
+
+ // configure retry wait time
+ String retryS = parameters.get(SocketEndpointConstants.PARAMETER_RETRY);
+ if (retryS == null || retryS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class, SocketEndpointConstants.PARAMETER_RETRY,
+ String.valueOf(SocketEndpointConstants.RETRY));
+ this.retry = SocketEndpointConstants.RETRY;
+ } else {
+ try {
+ this.retry = Long.parseLong(retryS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_RETRY, retryS);
+ }
+ }
+
+ // configure if timeout on connection should be activated
+ String useTimeoutS = parameters.get(SocketEndpointConstants.PARAMETER_USE_TIMEOUT);
+ if (useTimeoutS == null || useTimeoutS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_USE_TIMEOUT, String.valueOf(SocketEndpointConstants.USE_TIMEOUT));
+ this.useTimeout = SocketEndpointConstants.USE_TIMEOUT;
+ } else {
+ this.useTimeout = Boolean.parseBoolean(useTimeoutS);
+ }
+
+ if (this.useTimeout) {
+ // configure timeout on connection
+ String timeoutS = parameters.get(SocketEndpointConstants.PARAMETER_TIMEOUT);
+ if (timeoutS == null || timeoutS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_TIMEOUT, String.valueOf(SocketEndpointConstants.TIMEOUT));
+ this.timeout = SocketEndpointConstants.TIMEOUT;
+ } else {
+ try {
+ this.timeout = Integer.parseInt(timeoutS);
+ } catch (NumberFormatException e) {
+ throw ConnectionMessages.throwInvalidParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_TIMEOUT, timeoutS);
+ }
+ }
+ }
+
+ // configure if the connection should be cleared on connect
+ String clearOnConnectS = parameters.get(SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT);
+ if (clearOnConnectS == null || clearOnConnectS.length() == 0) {
+ ConnectionMessages.warnUnsetParameter(ServerSocketEndpoint.class,
+ SocketEndpointConstants.PARAMETER_CLEAR_ON_CONNECT,
+ String.valueOf(SocketEndpointConstants.CLEAR_ON_CONNECT));
+ this.clearOnConnect = SocketEndpointConstants.CLEAR_ON_CONNECT;
+ } else {
+ this.clearOnConnect = Boolean.parseBoolean(clearOnConnectS);
+ }
+ }
+
+ /**
+ * @return the uri as String to which this {@link ServerSocketEndpoint} is locally bound to
+ */
+ @Override
+ public String getLocalUri() {
+ if (this.socket != null) {
+ InetAddress localAddress = this.socket.getLocalAddress();
+ return localAddress.getHostAddress() + StringHelper.COLON + this.socket.getLocalPort();
+ } else if (this.localInputAddress != null) {
+ return this.localInputAddress.getHostAddress() + StringHelper.COLON + this.localInputPort;
+ }
+
+ return "0.0.0.0:0"; //$NON-NLS-1$
+ }
+
+ /**
+ * @return the uri as String from which this {@link ServerSocketEndpoint} is receiving data from
+ */
+ @Override
+ public String getRemoteUri() {
+ if (this.socket != null) {
+ InetAddress remoteAddress = this.socket.getInetAddress();
+ return remoteAddress.getHostAddress() + StringHelper.COLON + this.socket.getPort();
+ } else if (this.remoteOutputAddressS != null) {
+ return this.remoteOutputAddress.getHostAddress() + StringHelper.COLON + this.remoteOutputPort;
+ }
+
+ return "0.0.0.0:0"; //$NON-NLS-1$
+ }
+
+ /**
+ * Starts the {@link Thread} to allow incoming connections
+ *
+ * @see CommunicationEndpoint#start()
+ */
+ @Override
+ public void start() {
+
+ if (this.fatal) {
+ String msg = "CommunicationConnection had a fatal exception and can not yet be started. Please check log file for further information!"; //$NON-NLS-1$
+ throw new ConnectionException(msg);
+ }
+
+ if (this.serverThread != null) {
+ logger.warn(MessageFormat.format("CommunicationConnection {0} already started.", this.connection.getId())); //$NON-NLS-1$
+ } else {
+ // logger.info(MessageFormat.format("Enabling connection {0}...", this.connection.getId())); //$NON-NLS-1$
+ this.closed = false;
+
+ this.serverThread = new Thread(this, this.connection.getId());
+ this.serverThread.start();
+ }
+ }
+
+ /**
+ * Closes any open connection and then stops the {@link Thread} disallowing incoming connections
+ *
+ * @see CommunicationEndpoint#stop()
+ */
+ @Override
+ public void stop() {
+ closeThread();
+ closeConnection();
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTED.toString());
+
+ logger.info(MessageFormat.format("Disabled connection {0}.", this.connection.getId())); //$NON-NLS-1$
+ }
+
+ @Override
+ public void reset() {
+ closeThread();
+ closeConnection();
+ configure();
+ this.connection.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.toString());
+ }
+
+ private void closeThread() {
+ this.closed = true;
+ this.fatal = false;
+
+ if (this.serverThread != null) {
+ try {
+ this.serverThread.interrupt();
+ if (this.serverSocket != null)
+ this.serverSocket.close();
+ this.serverThread.join(2000l);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format(
+ "Exception while interrupting server thread: {0}", e.getLocalizedMessage())); //$NON-NLS-1$
+ }
+
+ this.serverThread = null;
+ }
+ }
+
+ /**
+ * Thread is listening on the ServerSocket and opens a new connection if necessary
+ */
+ @Override
+ public void run() {
+
+ while (!this.closed) {
+
+ // bomb-proof, catches all exceptions!
+ try {
+
+ // if serverSocket is null or closed, open a new server socket
+ if (this.serverSocket == null || this.serverSocket.isClosed()) {
+
+ try {
+ // String msg = "Opening socket on {0}:{1}..."; //$NON-NLS-1$
+ // logger.info(MessageFormat.format(msg, this.localInputAddress.getHostAddress(),
+ // Integer.toString(this.localInputPort)));
+ this.serverSocket = new ServerSocket(this.localInputPort, 1, this.localInputAddress);
+ this.serverSocket.setReuseAddress(true);
+ } catch (BindException e) {
+ logger.error("Fatal BindException occurred! Port is already in use, or address is illegal!"); //$NON-NLS-1$
+ logger.error(e.getMessage(), e);
+ this.closed = true;
+ this.fatal = true;
+
+ String msg = "Fatal error while binding to server socket. ServerSocket endpoint is dead"; //$NON-NLS-1$
+ throw new ConnectionException(msg);
+ }
+ }
+
+ // open the connection
+ openConnection();
+
+ // as long as connection is connected
+ while (checkConnection()) {
+
+ // read and write from the connected server socket
+ IoMessage message = this.messageVisitor.visit(this.inputStream, this.outputStream);
+ if (message != null) {
+ this.connection.handleNewMessage(message);
+ }
+ }
+
+ } catch (Exception e) {
+ if (e instanceof InterruptedException) {
+ logger.error("Interrupted!"); //$NON-NLS-1$
+ } else {
+ logger.error(e.getMessage(), e);
+ }
+ this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage());
+ } finally {
+ closeConnection();
+ }
+ }
+
+ if (!this.fatal) {
+ logger.warn(MessageFormat.format(
+ "CommunicationConnection {0} is not running anymore!", this.connection.getId())); //$NON-NLS-1$
+ this.connection.notifyStateChange(ConnectionState.BROKEN, null);
+ } else {
+ String msg = "CommunicationConnection {0} is broken due to a fatal exception!"; //$NON-NLS-1$
+ logger.error(MessageFormat.format(msg, this.connection.getId()));
+ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null);
+ }
+ }
+
+ @Override
+ public void simulate(IoMessage message) throws Exception {
+ send(message);
+ }
+
+ @Override
+ public void send(IoMessage message) throws Exception {
+ String msg = "The Server Socket can not send messages, use the {0} implementation instead!"; //$NON-NLS-1$
+ throw new UnsupportedOperationException(MessageFormat.format(msg, ClientSocketEndpoint.class.getName()));
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java
new file mode 100644
index 000000000..df7173e46
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.tcpip;
+
+/**
+ * Constants used in the communication classes
+ *
+ * @author Robert von Burg
+ */
+public class SocketEndpointConstants {
+
+ public static final String PARAMETER_USE_TIMEOUT = "useTimeout"; //$NON-NLS-1$
+ public static final String PARAMETER_TIMEOUT = "timeout"; //$NON-NLS-1$
+ public static final String PARAMETER_RETRY = "retry"; //$NON-NLS-1$
+ public static final String PARAMETER_CLEAR_ON_CONNECT = "clearOnConnect"; //$NON-NLS-1$
+ public static final String PARAMETER_CONNECT_ON_START = "connectOnStart"; //$NON-NLS-1$
+ public static final String PARAMETER_CLOSE_AFTER_SEND = "closeAfterSend"; //$NON-NLS-1$
+
+ public static final String PARAMETER_REMOTE_OUTPUT_PORT = "remoteOutputPort"; //$NON-NLS-1$
+ public static final String PARAMETER_REMOTE_OUTPUT_ADDRESS = "remoteOutputAddress"; //$NON-NLS-1$
+ public static final String PARAMETER_LOCAL_INPUT_PORT = "localInputPort"; //$NON-NLS-1$
+ public static final String PARAMETER_LOCAL_INPUT_ADDRESS = "localInputAddress"; //$NON-NLS-1$
+
+ public static final String PARAMETER_LOCAL_OUTPUT_ADDRESS = "localOutputAddress"; //$NON-NLS-1$
+ public static final String PARAMETER_LOCAL_OUTPUT_PORT = "localOutputPort"; //$NON-NLS-1$
+ public static final String PARAMETER_REMOTE_INPUT_ADDRESS = "remoteInputAddress"; //$NON-NLS-1$
+ public static final String PARAMETER_REMOTE_INPUT_PORT = "remoteInputPort"; //$NON-NLS-1$
+
+ /**
+ * Time to wait in milliseconds before reestablishing a connection. Default is 60000ms
+ */
+ public static final long RETRY = 60000l;
+
+ /**
+ * The time after which a connection is deemed dead. Value is 60000ms
+ */
+ public static final int TIMEOUT = 60000;
+
+ /**
+ * Default is to use a timeout on socket connections, thus this value is true
+ */
+ public static final boolean USE_TIMEOUT = true;
+
+ /**
+ * Default is to not clear the input socket on connect, thus this value is false
+ */
+ public static final boolean CLEAR_ON_CONNECT = false;
+
+ /**
+ * Default is to connect on start of the connection
+ */
+ public static final boolean CONNECT_ON_START = true;
+
+ /**
+ * Default is to not close after sending
+ */
+ public static final boolean CLOSE_AFTER_SEND = false;
+
+ /**
+ * Default is to disconnect after a null message is received when reading from a TCP socket, thus this value is true
+ */
+ public static final boolean DISCONNECT_ON_NULL_MSG = true;
+
+ /**
+ * If {@link #DISCONNECT_ON_NULL_MSG} is activated, then this is the default time used to wait before reading again,
+ * which is 10000ms
+ */
+ public static final long WAIT_TIME_ON_NULL_MSG = 10000l;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java
new file mode 100644
index 000000000..52f535300
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2014 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.communication.tcpip;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.net.Socket;
+
+import ch.eitchnet.communication.IoMessage;
+import ch.eitchnet.communication.IoMessageVisitor;
+
+/**
+ * This {@link IoMessageVisitor} implements and endpoint connecting to a {@link Socket}.
+ *
+ * @author Robert von Burg
+ */
+public abstract class SocketMessageVisitor extends IoMessageVisitor {
+
+ protected final String connectionId;
+
+ public SocketMessageVisitor(String connectionId) {
+ this.connectionId = connectionId;
+ }
+
+ public String getConnectionId() {
+ return this.connectionId;
+ }
+
+ /**
+ * This method is called when a message is read from the underlying {@link Socket}
+ *
+ * @param inputStream
+ * @param outputStream
+ * @return
+ * @throws Exception
+ */
+ public abstract IoMessage visit(DataInputStream inputStream, DataOutputStream outputStream) throws Exception;
+
+ /**
+ * This method is called when a message is to be sent to the underlying connected endpoint
+ *
+ * @param inputStream
+ * @param outputStream
+ * @param message
+ * @throws Exception
+ */
+ public abstract void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message)
+ throws Exception;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConnectionCheck.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConnectionCheck.java
new file mode 100644
index 000000000..967cdacf3
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConnectionCheck.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Robert von Burg
+ */
+public class DbConnectionCheck {
+
+ private static final Logger logger = LoggerFactory.getLogger(DbConnectionCheck.class);
+ private Map dsMap;
+
+ /**
+ * @param dsMap
+ */
+ public DbConnectionCheck(Map dsMap) {
+ this.dsMap = dsMap;
+ }
+
+ /**
+ * Checks the connectivity to each of the configured {@link DbConnectionInfo}
+ *
+ * @throws DbException
+ */
+ public void checkConnections() throws DbException {
+ Collection values = this.dsMap.values();
+ for (DataSource ds : values) {
+
+ logger.info("Checking connection " + ds);
+
+ try (Connection con = ds.getConnection(); Statement st = con.createStatement();) {
+
+ try (ResultSet rs = st.executeQuery("select version()")) { //$NON-NLS-1$
+ if (rs.next()) {
+ logger.info(MessageFormat.format("Connected to: {0}", rs.getString(1))); //$NON-NLS-1$
+ }
+ }
+
+ } catch (SQLException e) {
+ String msg = "Failed to open DB connection to {0} due to: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, ds, e.getMessage());
+ throw new DbException(msg, e);
+ }
+ }
+
+ logger.info("All connections OK");
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConstants.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConstants.java
new file mode 100644
index 000000000..edec0cf3a
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConstants.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+/**
+ * @author Robert von Burg
+ */
+public class DbConstants {
+
+ public static final String PROP_DB_URL = "db.url"; //$NON-NLS-1$
+ public static final String PROP_DB_IGNORE_REALM = "db.ignore.realm"; //$NON-NLS-1$
+ public static final String PROP_DB_USERNAME = "db.username"; //$NON-NLS-1$
+ public static final String PROP_DB_PASSWORD = "db.password"; //$NON-NLS-1$
+ public static final String PROP_ALLOW_SCHEMA_CREATION = "allowSchemaCreation";
+ public static final String PROP_ALLOW_SCHEMA_MIGRATION = "allowSchemaMigration";
+ public static final String PROP_ALLOW_SCHEMA_DROP = "allowSchemaDrop";
+ public static final String PROP_ALLOW_DATA_INIT_ON_SCHEMA_CREATE = "allowDataInitOnSchemaCreate";
+ public static final String PROP_DB_VERSION = "db_version";
+ public static final String RESOURCE_DB_VERSION = "/{0}_db_version.properties";
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java
new file mode 100644
index 000000000..eb33af6f1
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+/**
+ * @author Robert von Burg
+ */
+public interface DbDataSourceBuilder {
+
+ public DataSource build(String realm, String url, String username, String password, Properties properties);
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbException.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbException.java
new file mode 100644
index 000000000..f1c6bb942
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+/**
+ * @author Robert von Burg
+ */
+public class DbException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param message
+ */
+ public DbException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message
+ * @param cause
+ */
+ public DbException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbMigrationState.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbMigrationState.java
new file mode 100644
index 000000000..f937ef344
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbMigrationState.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+/**
+ * @author Robert von Burg
+ */
+public enum DbMigrationState {
+ NOTHING, CREATED, MIGRATED, DROPPED_CREATED;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java
new file mode 100644
index 000000000..8f870fec0
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.db;
+
+import static ch.eitchnet.db.DbConstants.PROP_DB_VERSION;
+import static ch.eitchnet.db.DbConstants.RESOURCE_DB_VERSION;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.utils.Version;
+import ch.eitchnet.utils.dbc.DBC;
+import ch.eitchnet.utils.helper.FileHelper;
+
+/**
+ * @author Robert von Burg
+ */
+@SuppressWarnings(value = "nls")
+public class DbSchemaVersionCheck {
+
+ private static final Logger logger = LoggerFactory.getLogger(DbSchemaVersionCheck.class);
+ private String app;
+ private Class> ctxClass;
+ private boolean allowSchemaCreation;
+ private boolean allowSchemaMigration;
+ private boolean allowSchemaDrop;
+ private Map dbMigrationStates;
+
+ /**
+ * @param app
+ * @param ctxClass
+ * @param allowSchemaCreation
+ * @param allowSchemaDrop
+ */
+ public DbSchemaVersionCheck(String app, Class> ctxClass, boolean allowSchemaCreation,
+ boolean allowSchemaMigration, boolean allowSchemaDrop) {
+
+ DBC.PRE.assertNotEmpty("app may not be empty!", app);
+ DBC.PRE.assertNotNull("ctxClass may not be null!", ctxClass);
+
+ this.app = app;
+ this.ctxClass = ctxClass;
+ this.allowSchemaCreation = allowSchemaCreation;
+ this.allowSchemaMigration = allowSchemaMigration;
+ this.allowSchemaDrop = allowSchemaDrop;
+ this.dbMigrationStates = new HashMap<>();
+ }
+
+ /**
+ * @return the dbMigrationStates
+ */
+ public Map getDbMigrationStates() {
+ return this.dbMigrationStates;
+ }
+
+ public void checkSchemaVersion(Map dsMap) throws DbException {
+ for (Entry entry : dsMap.entrySet()) {
+ String realm = entry.getKey();
+ DataSource ds = entry.getValue();
+ DbMigrationState dbMigrationState = checkSchemaVersion(realm, ds);
+ this.dbMigrationStates.put(realm, dbMigrationState);
+ }
+ }
+
+ /**
+ * Returns true if the schema existed or was only migrated, false if the schema was created
+ *
+ * @param ds
+ * @param realm2
+ *
+ * @param connectionInfo
+ *
+ * @return true if the schema existed or was only migrated, false if the schema was created
+ *
+ * @throws DbException
+ */
+ public DbMigrationState checkSchemaVersion(String realm, DataSource ds) throws DbException {
+
+ logger.info(MessageFormat.format("[{0}:{1}] Checking Schema version for: {2}", this.app, realm, ds));
+
+ Version expectedDbVersion = getExpectedDbVersion(this.app, this.ctxClass);
+
+ try (Connection con = ds.getConnection()) {
+
+ // get current version
+ Version currentVersion = getCurrentVersion(con, this.app);
+ DbMigrationState migrationType = detectMigrationState(realm, expectedDbVersion, currentVersion);
+
+ switch (migrationType) {
+ case CREATED:
+ createSchema(con, realm, expectedDbVersion);
+ break;
+ case MIGRATED:
+ migrateSchema(con, realm, expectedDbVersion);
+ break;
+ case DROPPED_CREATED:
+ throw new DbException("Migration type " + migrationType + " not handled!");
+ case NOTHING:
+ // do nothing
+ default:
+ break;
+ }
+
+ con.commit();
+ return migrationType;
+
+ } catch (SQLException e) {
+ String msg = "Failed to open DB connection to {0} due to: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, ds, e.getMessage());
+ throw new DbException(msg, e);
+ }
+ }
+
+ /**
+ * @param con
+ * @param app
+ *
+ * @return
+ *
+ * @throws SQLException
+ */
+ public static Version getCurrentVersion(Connection con, String app) throws SQLException {
+
+ // first see if we have any schema
+ String sql = "select table_schema, table_name, table_type from information_schema.tables where table_name = ?";
+ try (PreparedStatement st = con.prepareStatement(sql)) {
+ st.setString(1, PROP_DB_VERSION);
+ if (!st.executeQuery().next())
+ return null;
+ }
+
+ // first find current version
+ sql = "select id, version from db_version where app = ? order by id desc;";
+ Version currentVersion = null;
+ try (PreparedStatement st = con.prepareStatement(sql)) {
+ st.setString(1, app);
+ try (ResultSet rs = st.executeQuery()) {
+ if (rs.next())
+ currentVersion = Version.valueOf(rs.getString(2));
+ }
+ }
+
+ return currentVersion;
+ }
+
+ /**
+ * @param realm
+ * @param expectedDbVersion
+ *
+ * @return
+ *
+ * @throws SQLException
+ * @throws DbException
+ */
+ public DbMigrationState detectMigrationState(String realm, Version expectedDbVersion, Version currentVersion)
+ throws SQLException, DbException {
+
+ // no version, then we need to create it
+ if (currentVersion == null)
+ return DbMigrationState.CREATED;
+
+ // otherwise parse the version
+ int compare = expectedDbVersion.compareTo(currentVersion);
+ if (compare == 0) {
+ String msg = "[{0}:{1}] Schema version {2} is the current version. No changes needed.";
+ msg = MessageFormat.format(msg, this.app, realm, currentVersion);
+ logger.info(msg);
+ return DbMigrationState.NOTHING;
+ } else if (compare > 0) {
+ String msg = "[{0}:{1}] Schema version is not current. Need to upgrade from {2} to {3}";
+ msg = MessageFormat.format(msg, this.app, realm, currentVersion, expectedDbVersion);
+ logger.warn(msg);
+ return DbMigrationState.MIGRATED;
+ }
+
+ throw new DbException(MessageFormat.format("[{0}:{1}]Current version {2} is later than expected version {3}",
+ this.app, realm, currentVersion, expectedDbVersion));
+ }
+
+ /**
+ * @param app
+ * @param ctxClass
+ *
+ * @return
+ *
+ * @throws DbException
+ */
+ public static Version getExpectedDbVersion(String app, Class> ctxClass) throws DbException {
+ Properties dbVersionProps = new Properties();
+
+ String dbVersionPropFile = MessageFormat.format(RESOURCE_DB_VERSION, app);
+
+ try (InputStream stream = ctxClass.getResourceAsStream(dbVersionPropFile);) {
+ DBC.PRE.assertNotNull(
+ MessageFormat.format("Resource file with name {0} does not exist!", dbVersionPropFile), stream);
+ dbVersionProps.load(stream);
+ } catch (IOException e) {
+ String msg = "Expected resource file {0} does not exist or is not a valid properties file: {1}";
+ msg = MessageFormat.format(msg, dbVersionPropFile, e.getMessage());
+ throw new DbException(msg, e);
+ }
+ String dbVersion = dbVersionProps.getProperty(PROP_DB_VERSION);
+ String msg = "Missing property {0} in resource file {1}";
+ DBC.PRE.assertNotEmpty(MessageFormat.format(msg, PROP_DB_VERSION, dbVersionPropFile), dbVersion);
+
+ return Version.valueOf(dbVersion);
+ }
+
+ /**
+ * @param scriptPrefix
+ * @param classLoader
+ * @param dbVersion
+ * @param type
+ *
+ * @return
+ *
+ * @throws DbException
+ */
+ public static String getSql(String scriptPrefix, Class> ctxClass, Version dbVersion, String type)
+ throws DbException {
+ String schemaResourceS = MessageFormat.format("/{0}_db_schema_{1}_{2}.sql", scriptPrefix, dbVersion, type);
+ try (InputStream stream = ctxClass.getResourceAsStream(schemaResourceS);) {
+ DBC.PRE.assertNotNull(
+ MessageFormat.format("Schema Resource file with name {0} does not exist!", schemaResourceS),
+ stream);
+ return FileHelper.readStreamToString(stream);
+ } catch (IOException e) {
+ throw new DbException("Schema creation resource file is missing or could not be read: " + schemaResourceS,
+ e);
+ }
+ }
+
+ /**
+ *
+ * @param realm
+ * the realm to create the schema for (a {@link DbConnectionInfo} must exist for it)
+ * @param dbVersion
+ * the version to upgrade to
+ * @param st
+ * the open database {@link Statement} to which the SQL statements will be written
+ *
+ * @return true if the schema was created, false if it was not
+ *
+ * @throws DbException
+ */
+ public void createSchema(Connection con, String realm, Version dbVersion) throws DbException {
+
+ if (!this.allowSchemaCreation) {
+ String msg = "[{0}:{1}] No schema exists, or is not valid. Schema generation is disabled, thus can not continue!";
+ msg = MessageFormat.format(msg, this.app, realm);
+ throw new DbException(msg);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Creating initial schema...", this.app, realm));
+
+ String sql = getSql(this.app, this.ctxClass, dbVersion, "initial");
+
+ try (Statement st = con.createStatement()) {
+ st.execute(sql);
+ } catch (SQLException e) {
+ logger.error("Failed to execute schema creation SQL: \n" + sql);
+ throw new DbException("Failed to execute schema generation SQL: " + e.getMessage(), e);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Successfully created schema with version {2}", this.app, realm,
+ dbVersion));
+ }
+
+ /**
+ * Upgrades the schema to the given version. If the current version is below the given version, then currently this
+ * method drops the schema and recreates it. Real migration must still be implemented
+ *
+ * @param realm
+ * the realm to migrate (a {@link DbConnectionInfo} must exist for it)
+ * @param dbVersion
+ * the version to upgrade to
+ *
+ * @return true if the schema was recreated, false if it was simply migrated
+ *
+ * @throws DbException
+ */
+ public void migrateSchema(Connection con, String realm, Version dbVersion) throws DbException {
+
+ if (!this.allowSchemaMigration) {
+ String msg = "[{0}:{1}] Schema is not valid. Schema migration is disabled, thus can not continue!";
+ msg = MessageFormat.format(msg, this.app, realm);
+ throw new DbException(msg);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Migrating schema...", this.app, realm));
+
+ String sql = getSql(this.app, this.ctxClass, dbVersion, "migration");
+ try (Statement st = con.createStatement()) {
+ st.execute(sql);
+ } catch (SQLException e) {
+ logger.error("Failed to execute schema migration SQL: \n" + sql);
+ throw new DbException("Failed to execute schema migration SQL: " + e.getMessage(), e);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Successfully migrated schema to version {2}", this.app, realm,
+ dbVersion));
+ }
+
+ /**
+ * @param realm
+ * the realm for which the schema must be dropped (a {@link DbConnectionInfo} must exist for it)
+ * @param dbVersion
+ * the version with which to to drop the schema
+ * @param st
+ * the open database {@link Statement} to which the SQL statements will be written
+ *
+ * @throws DbException
+ */
+ public void dropSchema(Connection con, String realm, Version dbVersion) throws DbException {
+
+ if (!this.allowSchemaDrop) {
+ String msg = "[{0}:{1}] Dropping Schema is disabled, but is required to upgrade current schema...";
+ msg = MessageFormat.format(msg, this.app, realm);
+ throw new DbException(msg);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Dropping existing schema...", this.app, realm));
+
+ String sql = getSql(this.app, this.ctxClass, dbVersion, "drop");
+ try (Statement st = con.createStatement()) {
+ st.execute(sql);
+ } catch (SQLException e) {
+ logger.error("Failed to execute schema drop SQL: \n" + sql);
+ throw new DbException("Failed to execute schema drop SQL: " + e.getMessage(), e);
+ }
+
+ logger.info(MessageFormat.format("[{0}:{1}] Successfully dropped schema with version {2}", this.app, realm,
+ dbVersion));
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClient.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClient.java
new file mode 100644
index 000000000..0924c8689
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClient.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.fileserver;
+
+import java.rmi.RemoteException;
+
+/**
+ * @author Robert von Burg
+ *
+ */
+public interface FileClient {
+
+ /**
+ * Remote method with which a client can push parts of files to the server. It is up to the client to send as many
+ * parts as needed, the server will write the parts to the associated file
+ *
+ * @param filePart
+ * the part of the file
+ * @throws RemoteException
+ * if something goes wrong with the remote call
+ */
+ public void uploadFilePart(FilePart filePart) throws RemoteException;
+
+ /**
+ * Remote method with which a client can delete files from the server. It only deletes single files if they exist
+ *
+ * @param fileDeletion
+ * the {@link FileDeletion} defining the deletion request
+ *
+ * @return true if the file was deleted, false if the file did not exist
+ *
+ * @throws RemoteException
+ * if something goes wrong with the remote call
+ */
+ public boolean deleteFile(FileDeletion fileDeletion) throws RemoteException;
+
+ /**
+ * Remote method which a client can request part of a file. The server will fill the given {@link FilePart} with a
+ * byte array of the file, with bytes from the file, respecting the desired offset. It is up to the client to call
+ * this method multiple times for the entire file. It is a decision of the concrete implementation how much data is
+ * returned in each part, the client may pass a request, but this is not definitive
+ *
+ * @param filePart
+ * the part of the file
+ *
+ * @return the same file part, yet with the part of the file requested as a byte array
+ *
+ * @throws RemoteException
+ * if something goes wrong with the remote call
+ */
+ public FilePart requestFile(FilePart filePart) throws RemoteException;
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java
new file mode 100644
index 000000000..102f205d6
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.fileserver;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.rmi.RemoteException;
+import java.text.MessageFormat;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.utils.helper.FileHelper;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * @author Robert von Burg
+ *
+ */
+public class FileClientUtil {
+
+ private static final Logger logger = LoggerFactory.getLogger(FileClientUtil.class);
+
+ /**
+ * @param rmiFileClient
+ * @param origFilePart
+ * @param dstFile
+ */
+ public static void downloadFile(FileClient rmiFileClient, FilePart origFilePart, File dstFile) {
+
+ // here we don't overwrite, the caller must make sure the destination file does not exist
+ if (dstFile.exists()) {
+ String msg = "The destination file {0} already exists. Delete it first, if you want to overwrite it!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, dstFile.getAbsolutePath());
+ throw new RuntimeException(msg);
+ }
+
+ try {
+
+ FilePart tmpPart = origFilePart;
+
+ int loops = 0;
+ int startLength = tmpPart.getPartLength();
+ while (true) {
+ loops += 1;
+
+ // get the next part
+ tmpPart = rmiFileClient.requestFile(tmpPart);
+
+ // validate length of data
+ if (tmpPart.getPartLength() != tmpPart.getPartBytes().length) {
+ String msg = "Invalid tmpPart. Part length is not as long as the bytes passed {0} / {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, tmpPart.getPartLength(), tmpPart.getPartBytes().length);
+ throw new RuntimeException(msg);
+ }
+
+ // validate offset is size of file
+ if (tmpPart.getPartOffset() != dstFile.length()) {
+ String msg = "The part offset $offset is not at the end of the file {0} / {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, tmpPart.getPartOffset(), dstFile.length());
+ throw new RuntimeException(msg);
+ }
+
+ // append the part
+ FileHelper.appendFilePart(dstFile, tmpPart.getPartBytes());
+
+ // update the offset
+ tmpPart.setPartOffset(tmpPart.getPartOffset() + tmpPart.getPartBytes().length);
+
+ // break if the offset is past the length of the file
+ if (tmpPart.getPartOffset() >= tmpPart.getFileLength())
+ break;
+ }
+
+ String msg = "{0}: {1}: Requested {2} parts. StartSize: {3} EndSize: {4}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, tmpPart.getFileType(), tmpPart.getFileName(), loops, startLength,
+ tmpPart.getPartLength());
+ logger.info(msg);
+
+ // validate that the offset is at the end of the file
+ if (tmpPart.getPartOffset() != origFilePart.getFileLength()) {
+ msg = "Offset {0} is not at file length {1} after reading all the file parts!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, tmpPart.getPartOffset(), origFilePart.getFileLength());
+ throw new RuntimeException(msg);
+ }
+
+ // now validate hashes
+ String dstFileHash = StringHelper.getHexString(FileHelper.hashFileSha256(dstFile));
+ if (!dstFileHash.equals(origFilePart.getFileHash())) {
+ msg = "Downloading the file {0} failed because the hashes don''t match. Expected: {1} / Actual: {2}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, origFilePart.getFileName(), origFilePart.getFileHash(), dstFileHash);
+ throw new RuntimeException(msg);
+ }
+
+ } catch (Exception e) {
+ if (e instanceof RuntimeException)
+ throw (RuntimeException) e;
+ String msg = "Downloading the file {0} failed because of an underlying exception {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, origFilePart.getFileName(), e.getLocalizedMessage());
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /**
+ * @param rmiFileClient
+ * @param srcFile
+ * @param fileType
+ */
+ public static void uploadFile(FileClient rmiFileClient, File srcFile, String fileType) {
+
+ // make sure the source file exists
+ if (!srcFile.canRead()) {
+ String msg = MessageFormat.format("The source file does not exist at {0}", srcFile.getAbsolutePath()); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ try (BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(srcFile));) {
+
+ // get the size of the file
+ long fileLength = srcFile.length();
+ String fileHash = StringHelper.getHexString(FileHelper.hashFileSha256(srcFile));
+
+ // create the file part to send
+ FilePart filePart = new FilePart(srcFile.getName(), fileType);
+ filePart.setFileLength(fileLength);
+ filePart.setFileHash(fileHash);
+
+ // define the normal size of the parts we're sending. The last part will naturally have a different size
+ int partLength;
+ if (fileLength > FileHandler.MAX_PART_SIZE)
+ partLength = FileHandler.MAX_PART_SIZE;
+ else
+ partLength = (int) fileLength;
+
+ // this is the byte array of data we're sending each time
+ byte[] bytes = new byte[partLength];
+
+ // open the stream to the file
+ int read = 0;
+ int offset = 0;
+
+ // loop by reading the number of bytes needed for each part
+ int loops = 0;
+ int startLength = partLength;
+ while (true) {
+ loops += 1;
+
+ // read the bytes into the array
+ read = inputStream.read(bytes);
+
+ // validate we read the expected number of bytes
+ if (read == -1)
+ throw new IOException("Something went wrong while reading the bytes as -1 was returned!"); //$NON-NLS-1$
+ if (read != bytes.length) {
+ String msg = "Something went wrong while reading the bytes as the wrong number of bytes were read. Expected {0} Actual: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, bytes.length, read);
+ throw new IOException(msg);
+ }
+
+ // set the fields on the FilePart
+ filePart.setPartBytes(bytes);
+ filePart.setPartOffset(offset);
+ filePart.setPartLength(bytes.length);
+
+ // and if this is the last part, then also set that
+ int nextOffset = offset + bytes.length;
+ if (nextOffset == fileLength)
+ filePart.setLastPart(true);
+
+ // now send the part to the server
+ rmiFileClient.uploadFilePart(filePart);
+
+ // if this was the last part, then break
+ if (filePart.isLastPart())
+ break;
+
+ // otherwise update the offset for the next part by also making sure the next part is not larger than
+ // the last part of the file
+ if (nextOffset + bytes.length > fileLength) {
+ long remaining = fileLength - nextOffset;
+ if (remaining > FileHandler.MAX_PART_SIZE) {
+ String msg = "Something went wrong as the remaining part {0} is larger than MAX_PART_SIZE {1}!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, remaining, FileHandler.MAX_PART_SIZE);
+ throw new RuntimeException(msg);
+ }
+ partLength = (int) remaining;
+ bytes = new byte[partLength];
+ }
+
+ // and save the offset for the next loop
+ offset = nextOffset;
+ }
+
+ String msg = "{0}: {1}: Sent {2} parts. StartSize: {3} EndSize: {4}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, filePart.getFileType(), filePart.getFileName(), loops, startLength,
+ filePart.getPartLength());
+ logger.info(msg);
+
+ } catch (Exception e) {
+ if (e instanceof RuntimeException)
+ throw (RuntimeException) e;
+ String msg = "Uploading the file {0} failed because of an underlying exception {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, srcFile.getAbsolutePath(), e.getLocalizedMessage());
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /**
+ * @param rmiFileClient
+ * @param fileDeletion
+ * @param dstFile
+ */
+ public static void deleteFile(FileClient rmiFileClient, FileDeletion fileDeletion, File dstFile) {
+
+ try {
+ rmiFileClient.deleteFile(fileDeletion);
+ } catch (RemoteException e) {
+ String msg = "Deleting the file {0} failed because of an underlying exception {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, fileDeletion.getFileName(), e.getLocalizedMessage());
+ throw new RuntimeException(msg);
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileDeletion.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileDeletion.java
new file mode 100644
index 000000000..b669aa39e
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileDeletion.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.fileserver;
+
+import java.io.Serializable;
+
+/**
+ * @author Robert von Burg
+ */
+public class FileDeletion implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private String fileName;
+ private String fileType;
+
+ /**
+ * @param fileName
+ * the name of the file to be deleted. This is either just the name, or a path relative to the type
+ * @param fileType
+ * the type of file to delete. This defines in which path the file resides
+ */
+ public FileDeletion(String fileName, String fileType) {
+ this.fileName = fileName;
+ this.fileType = fileType;
+ }
+
+ /**
+ * @return the fileType
+ */
+ public String getFileType() {
+ return this.fileType;
+ }
+
+ /**
+ * @return the fileName
+ */
+ public String getFileName() {
+ return this.fileName;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileHandler.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileHandler.java
new file mode 100644
index 000000000..bee06ab65
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileHandler.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.fileserver;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ch.eitchnet.utils.helper.FileHelper;
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * This class handles remote requests of clients to upload or download a file. Uploading a file is done by calling
+ * {@link #handleFilePart(FilePart)} and the downloading a file is done by calling {@link #requestFile(FilePart)}
+ *
+ * @author Robert von Burg
+ */
+public class FileHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(FileHandler.class);
+
+ /**
+ * DEF_PART_SIZE = default part size which is set to 1048576 bytes (1 MiB)
+ */
+ public static final int MAX_PART_SIZE = 1048576;
+
+ private String basePath;
+ private boolean verbose;
+
+ /**
+ *
+ */
+ public FileHandler(String basePath, boolean verbose) {
+
+ File basePathF = new File(basePath);
+ if (!basePathF.exists()) {
+ String msg = MessageFormat.format("Base Path does not exist {0}", basePathF.getAbsolutePath()); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+ if (!basePathF.canWrite()) {
+ String msg = MessageFormat.format("Can not write to base path {0}", basePathF.getAbsolutePath()); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ this.verbose = verbose;
+ this.basePath = basePath;
+ }
+
+ /**
+ * Method which a client can request part of a file. The server will fill the given {@link FilePart} with a byte
+ * array of the file, with bytes from the file, respecting the desired offset. It is up to the client to call this
+ * method multiple times for the entire file. It is a decision of the concrete implementation how much data is
+ * returned in each part, the client may pass a request, but this is not definitive
+ *
+ * @param filePart
+ * the part of the file
+ */
+ public FilePart requestFile(FilePart filePart) {
+
+ // validate file name is legal
+ String fileName = filePart.getFileName();
+ validateFileName(fileName);
+
+ // validate type is legal
+ String fileType = filePart.getFileType();
+ validateFileType(fileType);
+
+ // evaluate the path where the file should reside
+ String fileTypePath = this.basePath + "/" + fileType; //$NON-NLS-1$
+ File file = new File(fileTypePath, filePart.getFileName());
+
+ // now evaluate the file exists
+ String fileNotFoundMsg = "The file {0} could not be found in the location for files of type {1}"; //$NON-NLS-1$
+ if (!file.canRead()) {
+ String msg = fileNotFoundMsg;
+ msg = MessageFormat.format(msg, fileName, fileType);
+ throw new RuntimeException(msg);
+ }
+
+ // if this is the start of the file, then prepare the file part
+ long fileSize = file.length();
+ if (filePart.getPartOffset() == 0) {
+
+ // set the file length
+ filePart.setFileLength(fileSize);
+
+ // set the SHA256 of the file
+ filePart.setFileHash(StringHelper.getHexString(FileHelper.hashFileSha256(file)));
+ }
+
+ // variables defining the part of the file we're going to return
+ long requestOffset = filePart.getPartOffset();
+ int requestSize = filePart.getPartLength();
+ if (requestSize > FileHandler.MAX_PART_SIZE) {
+ String msg = "The requested part size {0} is greater than the allowed {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, requestSize, MAX_PART_SIZE);
+ throw new RuntimeException(msg);
+ }
+
+ // validate lengths and offsets
+ if (filePart.getFileLength() != fileSize) {
+ String msg = "The part request has a file size {0}, but the file is actually {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, filePart.getFileLength(), fileSize);
+ throw new RuntimeException(msg);
+ } else if (requestOffset > fileSize) {
+ String msg = "The requested file part offset {0} is greater than the size of the file {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, requestOffset, fileSize);
+ throw new RuntimeException(msg);
+ }
+ // Otherwise make sure the offset + request length is not larger than the actual file size.
+ // If it is then this is the end part
+ else if (requestOffset + requestSize >= fileSize) {
+
+ long remaining = fileSize - requestOffset;
+
+ // update request size to last part of file
+ long l = Math.min(requestSize, remaining);
+
+ // this is a fail safe
+ if (l > MAX_PART_SIZE) {
+ String msg = "Something went wrong. Min of requestSize and remaining is > MAX_PART_SIZE of {0}!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, MAX_PART_SIZE);
+ throw new RuntimeException(msg);
+ }
+
+ // this is the size of the array we want to return
+ requestSize = (int) l;
+ filePart.setPartLength(requestSize);
+ filePart.setLastPart(true);
+ }
+
+ // now read the part of the file and set it as bytes for the file part
+ try (FileInputStream fin = new FileInputStream(file);) {
+
+ // position the stream
+ long skip = fin.skip(requestOffset);
+ if (skip != requestOffset) {
+ String msg = MessageFormat.format("Asked to skip {0} but only skipped {1}", requestOffset, skip); //$NON-NLS-1$
+ throw new IOException(msg);
+ }
+
+ // read the data
+ byte[] bytes = new byte[requestSize];
+ int read = fin.read(bytes);
+ if (read != requestSize) {
+ String msg = MessageFormat.format("Asked to read {0} but only read {1}", requestSize, read); //$NON-NLS-1$
+ throw new IOException(msg);
+ }
+
+ // set the return result
+ filePart.setPartBytes(bytes);
+
+ } catch (FileNotFoundException e) {
+ String msg = MessageFormat.format(fileNotFoundMsg, fileName, fileType);
+ throw new RuntimeException(msg);
+ } catch (IOException e) {
+ String msg = "There was an error while reading from the file {0}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, fileName);
+ throw new RuntimeException(msg);
+ }
+
+ // we are returning the same object as the user gave us, just modified
+ if (this.verbose) {
+ String msg = "Read {0} for file {1}/{2}"; //$NON-NLS-1$
+ String fileSizeS = FileHelper.humanizeFileSize(filePart.getPartBytes().length);
+ msg = MessageFormat.format(msg, fileSizeS, fileType, fileName);
+ logger.info(msg);
+ }
+ return filePart;
+ }
+
+ /**
+ * Method with which a client can push parts of files to the server. It is up to the client to send as many parts as
+ * needed, the server will write the parts to the associated file
+ *
+ * @param filePart
+ * the part of the file
+ */
+ public void handleFilePart(FilePart filePart) {
+
+ // validate file name is legal
+ String fileName = filePart.getFileName();
+ validateFileName(fileName);
+
+ // validate type is legal
+ String fileType = filePart.getFileType();
+ validateFileType(fileType);
+
+ // evaluate the path where the file should reside
+ String fileTypePath = this.basePath + "/" + fileType; //$NON-NLS-1$
+ File dstFile = new File(fileTypePath, filePart.getFileName());
+
+ // if the file already exists, then this may not be a start part
+ if (filePart.getPartOffset() == 0 && dstFile.exists()) {
+ String msg = "The file {0} already exist for type {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, fileName, fileType);
+ throw new RuntimeException(msg);
+ }
+
+ // write the part
+ FileHelper.appendFilePart(dstFile, filePart.getPartBytes());
+
+ // if this is the last part, then validate the hashes
+ if (filePart.isLastPart()) {
+ String dstFileHash = StringHelper.getHexString(FileHelper.hashFileSha256(dstFile));
+ if (!dstFileHash.equals(filePart.getFileHash())) {
+ String msg = "Uploading the file {0} failed because the hashes don''t match. Expected: {1} / Actual: {2}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, filePart.getFileName(), filePart.getFileHash(), dstFileHash);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ if (this.verbose) {
+ String msg;
+ if (filePart.isLastPart())
+ msg = "Wrote {0} for part of file {1}/{2}"; //$NON-NLS-1$
+ else
+ msg = "Wrote {0} for last part of file {1}/{2}"; //$NON-NLS-1$
+ String fileSizeS = FileHelper.humanizeFileSize(filePart.getPartBytes().length);
+ msg = MessageFormat.format(msg, fileSizeS, fileType, fileName);
+ logger.info(msg);
+ }
+ }
+
+ /**
+ * Method with which a client can delete files from the server. It only deletes single files if they exist
+ *
+ * @param fileDeletion
+ * the {@link FileDeletion} defining the deletion request
+ *
+ * @return true if the file was deleted, false if the file did not exist
+ *
+ */
+ public boolean deleteFile(FileDeletion fileDeletion) {
+
+ // validate file name is legal
+ String fileName = fileDeletion.getFileName();
+ validateFileName(fileName);
+
+ // validate type is legal
+ String fileType = fileDeletion.getFileType();
+ validateFileType(fileType);
+
+ // evaluate the path where the file should reside
+ String fileTypePath = this.basePath + "/" + fileType; //$NON-NLS-1$
+ File fileToDelete = new File(fileTypePath, fileDeletion.getFileName());
+
+ // delete the file
+ boolean deletedFile = FileHelper.deleteFiles(new File[] { fileToDelete }, true);
+
+ String msg;
+ if (deletedFile)
+ msg = "Deleted file {1}/{2}"; //$NON-NLS-1$
+ else
+ msg = "Failed to delete file {1}/{2}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, fileType, fileName);
+ logger.info(msg);
+ return deletedFile;
+ }
+
+ /**
+ * Validates that the file name is legal, i.e. not empty or contains references up the tree
+ *
+ * @param fileName
+ */
+ private void validateFileName(String fileName) {
+
+ if (fileName == null || fileName.isEmpty()) {
+ throw new RuntimeException("The file name was not given! Can not find a file without a name!"); //$NON-NLS-1$
+ } else if (fileName.contains("/")) { //$NON-NLS-1$
+ String msg = "The given file name contains illegal characters. The file name may not contain slashes!"; //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+ }
+
+ /**
+ * Validates that the file type is legal, i.e. not empty or contains references up the tree
+ *
+ * @param fileType
+ */
+ private void validateFileType(String fileType) {
+ if (fileType == null || fileType.isEmpty()) {
+ throw new RuntimeException("The file type was not given! Can not find a file without a type!"); //$NON-NLS-1$
+ } else if (fileType.contains("/")) { //$NON-NLS-1$
+ String msg = "The given file type contains illegal characters. The file type may not contain slashes!"; //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FilePart.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FilePart.java
new file mode 100644
index 000000000..cfd2f5ce3
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FilePart.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.fileserver;
+
+import java.io.Serializable;
+
+/**
+ * @author Robert von Burg
+ */
+public class FilePart implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private String fileName;
+ private long fileLength;
+ private String fileHash;
+ private String fileType;
+
+ private long partOffset;
+ private int partLength;
+ private byte[] partBytes;
+ private boolean lastPart;
+
+ /**
+ * @param fileName
+ * the name of the file being referenced. This is either just the name, or a path relative to the type
+ * @param fileType
+ * defines the type of file being uploaded or retrieved. This defines in which path the file resides
+ */
+ public FilePart(String fileName, String fileType) {
+
+ if (fileName == null || fileName.isEmpty())
+ throw new RuntimeException("fileName may not be empty!"); //$NON-NLS-1$
+ if (fileType == null || fileType.isEmpty())
+ throw new RuntimeException("fileType may not be empty!"); //$NON-NLS-1$
+
+ this.fileName = fileName;
+ this.fileType = fileType;
+
+ this.partOffset = 0;
+ this.partLength = FileHandler.MAX_PART_SIZE;
+ this.partBytes = null;
+ }
+
+ /**
+ * @return the fileLength
+ */
+ public long getFileLength() {
+ return this.fileLength;
+ }
+
+ /**
+ * @param fileLength
+ * the fileLength to set
+ */
+ public void setFileLength(long fileLength) {
+ this.fileLength = fileLength;
+ }
+
+ /**
+ * @return the fileHash
+ */
+ public String getFileHash() {
+ return this.fileHash;
+ }
+
+ /**
+ * @param fileHash
+ * the fileHash to set
+ */
+ public void setFileHash(String fileHash) {
+ this.fileHash = fileHash;
+ }
+
+ /**
+ * @return the fileType
+ */
+ public String getFileType() {
+ return this.fileType;
+ }
+
+ /**
+ * @return the partOffset
+ */
+ public long getPartOffset() {
+ return this.partOffset;
+ }
+
+ /**
+ * @param partOffset
+ * the partOffset to set
+ */
+ public void setPartOffset(long partOffset) {
+ this.partOffset = partOffset;
+ }
+
+ /**
+ * @return the partLength
+ */
+ public int getPartLength() {
+ return this.partLength;
+ }
+
+ /**
+ * @param partLength
+ * the partLength to set
+ */
+ public void setPartLength(int partLength) {
+ this.partLength = partLength;
+ }
+
+ /**
+ * @return the partBytes
+ */
+ public byte[] getPartBytes() {
+ return this.partBytes;
+ }
+
+ /**
+ * @param partBytes
+ * the partBytes to set
+ */
+ public void setPartBytes(byte[] partBytes) {
+ this.partBytes = partBytes;
+ }
+
+ /**
+ * @return the lastPart
+ */
+ public boolean isLastPart() {
+ return this.lastPart;
+ }
+
+ /**
+ * @param lastPart
+ * the lastPart to set
+ */
+ public void setLastPart(boolean lastPart) {
+ this.lastPart = lastPart;
+ }
+
+ /**
+ * @return the fileName
+ */
+ public String getFileName() {
+ return this.fileName;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/StringMatchMode.java
new file mode 100644
index 000000000..aec70ad28
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/StringMatchMode.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils;
+
+import ch.eitchnet.utils.dbc.DBC;
+
+/**
+ * @author Robert von Burg
+ */
+public enum StringMatchMode {
+ EQUALS_CASE_SENSITIVE(true, true),
+ EQUALS_CASE_INSENSITIVE(true, false),
+ CONTAINS_CASE_SENSITIVE(false, true),
+ CONTAINS_CASE_INSENSITIVE(false, false);
+
+ private final boolean equals;
+ private final boolean caseSensitve;
+
+ private StringMatchMode(boolean equals, boolean caseSensitive) {
+ this.equals = equals;
+ this.caseSensitve = caseSensitive;
+ }
+
+ /**
+ * @return the caseSensitve
+ */
+ public boolean isCaseSensitve() {
+ return this.caseSensitve;
+ }
+
+ /**
+ * @return the equals
+ */
+ public boolean isEquals() {
+ return this.equals;
+ }
+
+ public boolean matches(String value1, String value2) {
+ DBC.PRE.assertNotNull("value1 must be set!", value1); //$NON-NLS-1$
+ DBC.PRE.assertNotNull("value2 must be set!", value2); //$NON-NLS-1$
+ if (!isEquals() && !isCaseSensitve())
+ return value1.toLowerCase().contains(value2.toLowerCase());
+
+ if (!isCaseSensitve())
+ return value1.toLowerCase().equals(value2.toLowerCase());
+
+ if (!isEquals())
+ return value1.contains(value2);
+
+ return value1.equals(value2);
+ }
+
+ /**
+ *
+ * Short method, useful for static imports, or simply for shorter code
+ *
+ *
+ * @return {@link #CONTAINS_CASE_INSENSITIVE}
+ */
+ public static StringMatchMode ci() {
+ return CONTAINS_CASE_INSENSITIVE;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/Version.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/Version.java
new file mode 100644
index 000000000..7b6b93dd6
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/Version.java
@@ -0,0 +1,507 @@
+package ch.eitchnet.utils;
+
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+import ch.eitchnet.utils.helper.StringHelper;
+
+/**
+ * This class has been adapted from org.osgi.framework.Version
+ *
+ * Version identifier.
+ *
+ *
+ * Version identifiers have four components.
+ *
+ *
Major version. A non-negative integer.
+ *
Minor version. A non-negative integer.
+ *
Micro version. A non-negative integer.
+ *
Qualifier. A text string. See {@code Version(String)} for the format of the qualifier string.
+ *
+ *
+ * Note: The qualifier can be separated by two different styles: {@link #OSGI_QUALIFIER_SEPARATOR} or
+ * {@link #MAVEN_QUALIFIER_SEPARATOR}. Thus the qualifier my also have two special values:
+ * {@link #OSGI_SNAPSHOT_QUALIFIER} or {@value #MAVEN_SNAPSHOT_QUALIFIER}.
+ *
+ *
+ * The grammar for parsing version strings is as follows:
+ *
+ *
+ *
+ * Note: There must be no whitespace in version.
+ *
+ *
+ *
+ * Note: {@code Version} objects are immutable and thus thread safe
+ *
+ */
+public class Version implements Comparable {
+
+ public static final String SEPARATOR = ".";
+ public static final String OSGI_QUALIFIER_SEPARATOR = ".";
+ public static final String MAVEN_QUALIFIER_SEPARATOR = "-";
+
+ public static final String MAVEN_SNAPSHOT_QUALIFIER = "SNAPSHOT";
+ public static final String OSGI_SNAPSHOT_QUALIFIER = "qualifier";
+
+ private final int major;
+ private final int minor;
+ private final int micro;
+ private final String qualifier;
+
+ private transient String versionString;
+ private boolean osgiStyle;
+
+ /**
+ * The empty version "0.0.0".
+ */
+ public static final Version emptyVersion = new Version(0, 0, 0);
+
+ /**
+ * Creates a version identifier from the specified numerical components. This instance will have
+ * {@link #isOsgiStyle()} return false
+ *
+ *
+ * The qualifier is set to the empty string.
+ *
+ * @param major
+ * Major component of the version identifier.
+ * @param minor
+ * Minor component of the version identifier.
+ * @param micro
+ * Micro component of the version identifier.
+ * @throws IllegalArgumentException
+ * If the numerical components are negative.
+ */
+ public Version(final int major, final int minor, final int micro) {
+ this(major, minor, micro, null);
+ }
+
+ /**
+ * Creates a version identifier from the specified components. This instance will have {@link #isOsgiStyle()} return
+ * false
+ *
+ * @param major
+ * Major component of the version identifier.
+ * @param minor
+ * Minor component of the version identifier.
+ * @param micro
+ * Micro component of the version identifier.
+ * @param qualifier
+ * Qualifier component of the version identifier. If {@code null} is specified, then the qualifier will
+ * be set to the empty string.
+ *
+ * @throws IllegalArgumentException
+ * If the numerical components are negative or the qualifier string is invalid.
+ */
+ public Version(final int major, final int minor, final int micro, String qualifier) {
+ this(major, minor, micro, null, false);
+ }
+
+ /**
+ * Creates a version identifier from the specified components.
+ *
+ * @param major
+ * Major component of the version identifier.
+ * @param minor
+ * Minor component of the version identifier.
+ * @param micro
+ * Micro component of the version identifier.
+ * @param qualifier
+ * Qualifier component of the version identifier. If {@code null} is specified, then the qualifier will
+ * be set to the empty string.
+ * @param osgiStyle
+ * if true, then this is an osgi style version, otherwise not
+ *
+ * @throws IllegalArgumentException
+ * If the numerical components are negative or the qualifier string is invalid.
+ */
+ public Version(final int major, final int minor, final int micro, String qualifier, boolean osgiStyle) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ this.qualifier = qualifier == null ? "" : qualifier;
+ this.versionString = null;
+ validate();
+ }
+
+ /**
+ *
+ * Creates a version identifier from the specified string.
+ *
+ *
+ * @param version
+ * String representation of the version identifier.
+ *
+ * @throws IllegalArgumentException
+ * If {@code version} is improperly formatted.
+ */
+ private Version(final String version) {
+ int maj = 0;
+ int min = 0;
+ int mic = 0;
+ String qual = StringHelper.EMPTY;
+
+ try {
+ StringTokenizer st = new StringTokenizer(version,
+ SEPARATOR + MAVEN_QUALIFIER_SEPARATOR + OSGI_QUALIFIER_SEPARATOR, true);
+ maj = Integer.parseInt(st.nextToken());
+
+ if (st.hasMoreTokens()) { // minor
+ st.nextToken(); // consume delimiter
+ min = Integer.parseInt(st.nextToken());
+
+ if (st.hasMoreTokens()) { // micro
+ st.nextToken(); // consume delimiter
+ mic = Integer.parseInt(st.nextToken());
+
+ if (st.hasMoreTokens()) { // qualifier
+
+ String qualifierSeparator = st.nextToken(); // consume delimiter
+ this.osgiStyle = qualifierSeparator.equals(OSGI_QUALIFIER_SEPARATOR);
+
+ qual = st.nextToken(StringHelper.EMPTY); // remaining string
+
+ if (st.hasMoreTokens()) { // fail safe
+ throw new IllegalArgumentException("invalid format: " + version);
+ }
+ }
+ }
+ }
+ } catch (NoSuchElementException e) {
+ IllegalArgumentException iae = new IllegalArgumentException("invalid format: " + version);
+ iae.initCause(e);
+ throw iae;
+ }
+
+ this.major = maj;
+ this.minor = min;
+ this.micro = mic;
+ this.qualifier = qual;
+ this.versionString = null;
+ validate();
+ }
+
+ /**
+ * Called by the Version constructors to validate the version components.
+ *
+ * @throws IllegalArgumentException
+ * If the numerical components are negative or the qualifier string is invalid.
+ */
+ private void validate() {
+ if (this.major < 0) {
+ throw new IllegalArgumentException("negative major");
+ }
+ if (this.minor < 0) {
+ throw new IllegalArgumentException("negative minor");
+ }
+ if (this.micro < 0) {
+ throw new IllegalArgumentException("negative micro");
+ }
+ char[] chars = this.qualifier.toCharArray();
+ for (char ch : chars) {
+ if (('A' <= ch) && (ch <= 'Z')) {
+ continue;
+ }
+ if (('a' <= ch) && (ch <= 'z')) {
+ continue;
+ }
+ if (('0' <= ch) && (ch <= '9')) {
+ continue;
+ }
+ if ((ch == '_') || (ch == '-')) {
+ continue;
+ }
+ throw new IllegalArgumentException("invalid qualifier: " + this.qualifier);
+ }
+ }
+
+ public Boolean isFullyQualified() {
+ return !this.qualifier.isEmpty();
+ }
+
+ /**
+ * Parses a version identifier from the specified string.
+ *
+ *
+ * See {@code Version(String)} for the format of the version string.
+ *
+ * @param version
+ * String representation of the version identifier. Leading and trailing whitespace will be ignored.
+ *
+ * @return A {@code Version} object representing the version identifier. If {@code version} is {@code null} or the
+ * empty string then {@code emptyVersion} will be returned.
+ *
+ * @throws IllegalArgumentException
+ * If {@code version} is improperly formatted.
+ */
+ public static Version valueOf(String version) {
+ if (version == null)
+ return emptyVersion;
+
+ String trimmedVersion = version.trim();
+ if (trimmedVersion.length() == 0)
+ return emptyVersion;
+
+ return new Version(trimmedVersion);
+ }
+
+ /**
+ * Returns true if the given version string can be parsed, meaning a {@link Version} instance can be instantiated
+ * with it
+ *
+ * @param version
+ * String representation of the version identifier. Leading and trailing whitespace will be ignored.
+ *
+ * @return true if no parse errors occurr
+ */
+ public static boolean isParseable(String version) {
+ try {
+ valueOf(version);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the major component of this version identifier.
+ *
+ * @return The major component.
+ */
+ public int getMajor() {
+ return this.major;
+ }
+
+ /**
+ * Returns the minor component of this version identifier.
+ *
+ * @return The minor component.
+ */
+ public int getMinor() {
+ return this.minor;
+ }
+
+ /**
+ * Returns the micro component of this version identifier.
+ *
+ * @return The micro component.
+ */
+ public int getMicro() {
+ return this.micro;
+ }
+
+ /**
+ * Returns the qualifier component of this version identifier.
+ *
+ * @return The qualifier component.
+ */
+ public String getQualifier() {
+ return this.qualifier;
+ }
+
+ /**
+ * Returns a new {@link Version} where each version number is incremented or decreased by the given parameters
+ *
+ * @param major
+ * the value to increase or decrease the major part of the version
+ * @param minor
+ * the value to increase or decrease the minor part of the version
+ * @param micro
+ * the value to increase or decrease the micro part of the version
+ *
+ * @return the new Version with the version parts modified as passed in by the parameters
+ */
+ public Version add(int major, int minor, int micro) {
+ return new Version(this.major + major, this.minor + minor, this.micro + micro, this.qualifier, this.osgiStyle);
+ }
+
+ /**
+ * @return true if this is an OSGI style version, i.e. if has a qualifier, then osgi defines how the qualifier is
+ * appended to the version
+ */
+ public boolean isOsgiStyle() {
+ return this.osgiStyle;
+ }
+
+ /**
+ * @return true if this version is for a snapshot version, i.e. ends with {@link #MAVEN_SNAPSHOT_QUALIFIER} or
+ * {@link #OSGI_SNAPSHOT_QUALIFIER}
+ */
+ public boolean isSnapshot() {
+ return MAVEN_SNAPSHOT_QUALIFIER.equals(this.qualifier) || OSGI_SNAPSHOT_QUALIFIER.equals(this.qualifier);
+ }
+
+ /**
+ * Returns a hash code value for the object.
+ *
+ * @return An integer which is a hash code value for this object.
+ */
+ @Override
+ public int hashCode() {
+ return (this.major << 24) + (this.minor << 16) + (this.micro << 8) + this.qualifier.hashCode();
+ }
+
+ /**
+ * Compares this {@code Version} object to another object.
+ *
+ *
+ * A version is considered to be equal to another version if the major, minor and micro components are equal
+ * and the qualifier component is equal (using {@code String.equals}).
+ *
+ * @param object
+ * The {@code Version} object to be compared.
+ * @return {@code true} if {@code object} is a {@code Version} and is equal to this object; {@code false} otherwise.
+ */
+ @Override
+ public boolean equals(final Object object) {
+ if (object == this)
+ return true;
+ if (!(object instanceof Version))
+ return false;
+
+ Version other = (Version) object;
+ return (this.major == other.major) && (this.minor == other.minor) && (this.micro == other.micro)
+ && this.qualifier.equals(other.qualifier);
+ }
+
+ /**
+ * Compares this {@code Version} object to another object ignoring the qualifier part.
+ *
+ *
+ * A version is considered to be equal to another version if the major, minor and micro components are
+ * equal.
+ *
+ * @param object
+ * The {@code Version} object to be compared.
+ * @return {@code true} if {@code object} is a {@code Version} and is equal to this object; {@code false} otherwise.
+ */
+ public boolean equalsIgnoreQualifier(final Object object) {
+ if (object == this)
+ return true;
+ if (!(object instanceof Version))
+ return false;
+
+ Version other = (Version) object;
+ return (this.major == other.major) && (this.minor == other.minor) && (this.micro == other.micro);
+ }
+
+ /**
+ * Compares this {@code Version} object to another {@code Version}.
+ *
+ *
+ * A version is considered to be less than another version if its major component is less than the other
+ * version's major component, or the major components are equal and its minor component is less than the other
+ * version's minor component, or the major and minor components are equal and its micro component is less than the
+ * other version's micro component, or the major, minor and micro components are equal and it's qualifier component
+ * is less than the other version's qualifier component (using {@code String.compareTo}).
+ *
+ *
+ * A version is considered to be equal to another version if the major, minor and micro components are equal
+ * and the qualifier component is equal (using {@code String.compareTo}).
+ *
+ * @param other
+ * The {@code Version} object to be compared.
+ * @return A negative integer, zero, or a positive integer if this version is less than, equal to, or greater than
+ * the specified {@code Version} object.
+ * @throws ClassCastException
+ * If the specified object is not a {@code Version} object.
+ */
+ @Override
+ public int compareTo(final Version other) {
+ if (other == this)
+ return 0;
+
+ int result = this.major - other.major;
+ if (result != 0)
+ return result;
+
+ result = this.minor - other.minor;
+ if (result != 0)
+ return result;
+
+ result = this.micro - other.micro;
+ if (result != 0)
+ return result;
+
+ return this.qualifier.compareTo(other.qualifier);
+ }
+
+ /**
+ * Returns the string representation of this version identifier.
+ *
+ *
+ * The format of the version string will be {@code major.minor.micro} if qualifier is the empty string or
+ * {@code major.minor.micro.qualifier} otherwise.
+ *
+ * @return The string representation of this version identifier.
+ */
+ @Override
+ public String toString() {
+ if (this.versionString == null)
+ this.versionString = toString(this.osgiStyle);
+ return this.versionString;
+ }
+
+ private String toString(final boolean withOsgiStyle) {
+ int q = this.qualifier.length();
+ StringBuilder result = new StringBuilder(20 + q);
+ result.append(this.major);
+ result.append(SEPARATOR);
+ result.append(this.minor);
+ result.append(SEPARATOR);
+ result.append(this.micro);
+ if (q > 0) {
+ if (withOsgiStyle) {
+ result.append(OSGI_QUALIFIER_SEPARATOR);
+ } else {
+ result.append(MAVEN_QUALIFIER_SEPARATOR);
+ }
+ result.append(createQualifier(withOsgiStyle));
+ }
+ return result.toString();
+ }
+
+ private String createQualifier(boolean withOsgiStyle) {
+ if (this.qualifier.equals(MAVEN_SNAPSHOT_QUALIFIER) || this.qualifier.equals(OSGI_SNAPSHOT_QUALIFIER)) {
+ if (withOsgiStyle)
+ return OSGI_SNAPSHOT_QUALIFIER;
+ return MAVEN_SNAPSHOT_QUALIFIER;
+ }
+
+ return this.qualifier;
+ }
+
+ /**
+ * @return This version represented in a maven compatible form.
+ */
+ public String toMavenStyleString() {
+ return toString(false);
+ }
+
+ /**
+ * @return This version represented in an OSGi compatible form.
+ */
+ public String toOsgiStyleString() {
+ return toString(true);
+ }
+
+ /**
+ * @return This only the major and minor version in a string
+ */
+ public String toMajorAndMinorString() {
+ StringBuilder result = new StringBuilder(20);
+ result.append(this.major);
+ result.append(SEPARATOR);
+ result.append(this.minor);
+ return result.toString();
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DateRange.java
new file mode 100644
index 000000000..b4410b8a9
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DateRange.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.collections;
+
+import java.util.Date;
+
+import ch.eitchnet.utils.dbc.DBC;
+import ch.eitchnet.utils.iso8601.ISO8601FormatFactory;
+
+/**
+ * @author Robert von Burg
+ */
+public class DateRange {
+
+ private boolean fromInclusive;
+ private boolean toInclusive;
+ private Date fromDate;
+ private Date toDate;
+
+ public DateRange from(Date from, boolean inclusive) {
+ this.fromDate = from;
+ this.fromInclusive = inclusive;
+ validate();
+ return this;
+ }
+
+ public DateRange to(Date to, boolean inclusive) {
+ this.toDate = to;
+ this.toInclusive = inclusive;
+ validate();
+ return this;
+ }
+
+ private void validate() {
+ if (this.toDate != null && this.fromDate != null)
+ DBC.INTERIM.assertTrue("From must be before to!", this.toDate.compareTo(this.fromDate) >= 0); //$NON-NLS-1$
+ }
+
+ /**
+ * @return from date
+ */
+ public Date getFromDate() {
+ return this.fromDate;
+ }
+
+ /**
+ * @return to date
+ */
+ public Date getToDate() {
+ return this.toDate;
+ }
+
+ /**
+ * @return true if from is set
+ */
+ public boolean isFromBounded() {
+ return this.fromDate != null;
+ }
+
+ /**
+ * @return true if to is set
+ */
+ public boolean isToBounded() {
+ return this.toDate != null;
+ }
+
+ /**
+ * @return true if both from and to are null
+ */
+ public boolean isUnbounded() {
+ return this.fromDate == null && this.toDate == null;
+ }
+
+ /**
+ * @return true if both from and to date are set
+ */
+ public boolean isBounded() {
+ return this.fromDate != null && this.toDate != null;
+ }
+
+ /**
+ * @return true if both from and to date are set and they are both equal
+ */
+ public boolean isDate() {
+ return isBounded() && this.fromDate.equals(this.toDate);
+ }
+
+ public boolean contains(Date date) {
+ DBC.PRE.assertNotNull("Date must be given!", date); //$NON-NLS-1$
+ if (this.fromDate == null && this.toDate == null)
+ return true;
+
+ boolean fromContains = true;
+ boolean toContains = true;
+
+ if (this.toDate != null) {
+ int compare = this.toDate.compareTo(date);
+ if (this.toInclusive)
+ toContains = compare >= 0;
+ else
+ toContains = compare > 0;
+ }
+
+ if (this.fromDate != null) {
+ int compare = this.fromDate.compareTo(date);
+ if (this.fromInclusive)
+ fromContains = compare <= 0;
+ else
+ fromContains = compare < 0;
+ }
+ return toContains && fromContains;
+ }
+
+ @Override
+ public String toString() {
+ ISO8601FormatFactory df = ISO8601FormatFactory.getInstance();
+ return df.formatDate(this.fromDate) + (this.fromInclusive ? " (inc)" : " (exc)") + " - "
+ + df.formatDate(this.toDate) + (this.toInclusive ? " (inc)" : " (exc)");
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java
new file mode 100644
index 000000000..10ef2d75b
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.collections;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Robert von Burg
+ */
+public class DefaultedHashMap extends HashMap {
+
+ private static final long serialVersionUID = 1L;
+ private V defaultValue;
+
+ /**
+ * Constructs this {@link Map} instance to have a default value on inexistent keys
+ *
+ * @param defaultValue
+ * the default to return if a key is not mapped
+ */
+ public DefaultedHashMap(V defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public V get(Object key) {
+ return getOrDefault(key, this.defaultValue);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java
new file mode 100644
index 000000000..e40c9a849
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.collections;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * @author Robert von Burg
+ */
+public class MapOfLists {
+
+ private Map> mapOfLists;
+
+ public MapOfLists() {
+ this.mapOfLists = new HashMap<>();
+ }
+
+ public Set keySet() {
+ return this.mapOfLists.keySet();
+ }
+
+ public List getList(T t) {
+ return this.mapOfLists.get(t);
+ }
+
+ public boolean addElement(T t, U u) {
+ List list = this.mapOfLists.get(t);
+ if (list == null) {
+ list = new ArrayList<>();
+ this.mapOfLists.put(t, list);
+ }
+ return list.add(u);
+ }
+
+ public boolean addList(T t, List u) {
+ List list = this.mapOfLists.get(t);
+ if (list == null) {
+ list = new ArrayList<>();
+ this.mapOfLists.put(t, list);
+ }
+ return list.addAll(u);
+ }
+
+ public boolean removeElement(T t, U u) {
+ List list = this.mapOfLists.get(t);
+ if (list == null) {
+ return false;
+ }
+ boolean removed = list.remove(u);
+ if (list.isEmpty()) {
+ this.mapOfLists.remove(t);
+ }
+
+ return removed;
+ }
+
+ public List removeList(T t) {
+ return this.mapOfLists.remove(t);
+ }
+
+ public void clear() {
+ Set>> entrySet = this.mapOfLists.entrySet();
+ Iterator>> iter = entrySet.iterator();
+ while (iter.hasNext()) {
+ iter.next().getValue().clear();
+ iter.remove();
+ }
+ }
+
+ public boolean containsList(T t) {
+ return this.mapOfLists.containsKey(t);
+ }
+
+ public boolean containsElement(T t, U u) {
+ List list = this.mapOfLists.get(t);
+ if (list == null)
+ return false;
+ return list.contains(u);
+ }
+
+ public int sizeKeys() {
+ return this.mapOfLists.size();
+ }
+
+ public int size() {
+ int size = 0;
+ Set>> entrySet = this.mapOfLists.entrySet();
+ Iterator>> iter = entrySet.iterator();
+ while (iter.hasNext()) {
+ size += iter.next().getValue().size();
+ }
+ return size;
+ }
+
+ public int size(T t) {
+ List list = this.mapOfLists.get(t);
+ if (list.size() == 0)
+ return 0;
+ return list.size();
+ }
+
+ public boolean isEmpty() {
+ return this.mapOfLists.isEmpty();
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java
new file mode 100644
index 000000000..724caa53d
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.collections;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ *
+ * Collection to store a tree with a depth of 3 elements. This solves having to always write the declaration:
+ *
+ * The following implementations are supported:
+ *
+ *
Base64
+ *
Base64 URL safe
+ *
Base32
+ *
Base32 HEX
+ *
Base16 / HEX
+ *
+ *
+ *
+ *
+ * As a further bonus, it is possible to use the algorithm with a client specified alphabet. In this case the client is
+ * responsible for generating the alphabet for use in the decoding
+ *
+ *
+ *
+ * This class also implements a number of utility methods to check if given data is in a valid encoding
+ *
+ *
+ * @author Robert von Burg
+ */
+public class BaseEncoding {
+
+ // private static final Logger logger = LoggerFactory.getLogger(BaseEncoding.class);
+
+ private static final int PADDING_64 = 2;
+ private static final int PADDING_32 = 6;
+
+ public static final byte PAD = '=';
+
+ static final byte[] BASE_16 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+ static final byte[] BASE_32 = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7' };
+
+ static final byte[] BASE_32_HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
+ 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V' };
+
+ static final byte[] BASE_32_DMEDIA = { '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
+ 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y' };
+
+ static final byte[] BASE_32_CROCKFORD = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
+ 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z' };
+
+ static final byte[] BASE_64 = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
+ 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', '+', '/' };
+
+ static final byte[] BASE_64_SAFE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
+ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '-', '_' };
+
+ // these reverse base encoding alphabets were generated from the actual alphabet
+
+ static final byte[] REV_BASE_16 = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
+
+ static final byte[] REV_BASE_32 = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
+
+ static final byte[] REV_BASE_32_CROCKFORD = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17,
+ -1, 18, 19, -1, 20, 21, -1, 22, 23, 24, 25, 26, -1, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1 };
+
+ static final byte[] REV_BASE_32_DMEDIA = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, -1, -1, -1, -1, -1, -1, -1, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
+
+ static final byte[] REV_BASE_32_HEX = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
+
+ static final byte[] REV_BASE_64 = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1,
+ 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31,
+ 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 };
+
+ static final byte[] REV_BASE_64_SAFE = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ 62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29,
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 };
+
+ public static byte[] toBase64(byte[] bytes) {
+ return toBase64(BASE_64, bytes);
+ }
+
+ public static String toBase64(String data) {
+ return toBase64(BASE_64, data);
+ }
+
+ public static byte[] toBase64Safe(byte[] bytes) {
+ return toBase64(BASE_64_SAFE, bytes);
+ }
+
+ public static String toBase64Safe(String data) {
+ return toBase64(BASE_64_SAFE, data);
+ }
+
+ public static String toBase64(byte[] alphabet, String data) {
+ return new String(toBase64(alphabet, data.getBytes()));
+ }
+
+ public static byte[] toBase32(byte[] bytes) {
+ return toBase32(BASE_32, bytes);
+ }
+
+ public static String toBase32(String data) {
+ return toBase32(BASE_32, data);
+ }
+
+ public static byte[] toBase32Hex(byte[] bytes) {
+ return toBase32(BASE_32_HEX, bytes);
+ }
+
+ public static String toBase32Hex(String data) {
+ return toBase32(BASE_32_HEX, data);
+ }
+
+ public static byte[] toBase32Dmedia(byte[] bytes) {
+ return toBase32(BASE_32_DMEDIA, bytes);
+ }
+
+ public static String toBase32Dmedia(String data) {
+ return toBase32(BASE_32_DMEDIA, data);
+ }
+
+ public static byte[] toBase32Crockford(byte[] bytes) {
+ return toBase32(BASE_32_CROCKFORD, bytes);
+ }
+
+ public static String toBase32Crockford(String data) {
+ return toBase32(BASE_32_CROCKFORD, data);
+ }
+
+ public static String toBase32(byte[] alphabet, String data) {
+ return new String(toBase32(alphabet, data.getBytes()));
+ }
+
+ public static byte[] toBase16(byte[] bytes) {
+ return toBase16(BASE_16, bytes);
+ }
+
+ public static String toBase16(String data) {
+ return toBase16(BASE_16, data);
+ }
+
+ public static String toBase16(byte[] alphabet, String data) {
+ return new String(toBase16(alphabet, data.getBytes()));
+ }
+
+ public static byte[] fromBase64(byte[] bytes) {
+ return fromBase64(REV_BASE_64, bytes);
+ }
+
+ public static String fromBase64(String data) {
+ return fromBase64(REV_BASE_64, data);
+ }
+
+ public static String fromBase64Safe(String data) {
+ return fromBase64(REV_BASE_64_SAFE, data);
+ }
+
+ public static String fromBase64(byte[] alphabet, String data) {
+ return new String(fromBase64(alphabet, data.getBytes()));
+ }
+
+ public static byte[] fromBase32(byte[] bytes) {
+ return fromBase32(REV_BASE_32, bytes);
+ }
+
+ public static String fromBase32(String data) {
+ return fromBase32(REV_BASE_32, data);
+ }
+
+ public static byte[] fromBase32Hex(byte[] bytes) {
+ return fromBase32(REV_BASE_32_HEX, bytes);
+ }
+
+ public static String fromBase32Hex(String data) {
+ return fromBase32(REV_BASE_32_HEX, data);
+ }
+
+ public static byte[] fromBase32Dmedia(byte[] bytes) {
+ return fromBase32(REV_BASE_32_DMEDIA, bytes);
+ }
+
+ public static String fromBase32Dmedia(String data) {
+ return fromBase32(REV_BASE_32_DMEDIA, data);
+ }
+
+ public static byte[] fromBase32Crockford(byte[] bytes) {
+ return fromBase32(REV_BASE_32_CROCKFORD, bytes);
+ }
+
+ public static String fromBase32Crockford(String data) {
+ return fromBase32(REV_BASE_32_CROCKFORD, data);
+ }
+
+ public static String fromBase32(byte[] alphabet, String data) {
+ return new String(fromBase32(alphabet, data.getBytes()));
+ }
+
+ public static byte[] fromBase16(byte[] bytes) {
+ return fromBase16(REV_BASE_16, bytes);
+ }
+
+ public static String fromBase16(String data) {
+ return fromBase16(REV_BASE_16, data);
+ }
+
+ public static String fromBase16(byte[] alphabet, String data) {
+ return new String(fromBase16(alphabet, data.getBytes()));
+ }
+
+ public static boolean isBase64(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_64, bytes, PADDING_64);
+ }
+
+ public static boolean isBase64(String data) {
+ return isEncodedByAlphabet(REV_BASE_64, data, PADDING_64);
+ }
+
+ public static boolean isBase64Safe(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_64_SAFE, bytes, PADDING_64);
+ }
+
+ public static boolean isBase64Safe(String data) {
+ return isEncodedByAlphabet(REV_BASE_64_SAFE, data, PADDING_64);
+ }
+
+ public static boolean isBase32(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_32, bytes, PADDING_32);
+ }
+
+ public static boolean isBase32(String data) {
+ return isEncodedByAlphabet(REV_BASE_32, data, PADDING_32);
+ }
+
+ public static boolean isBase32Hex(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_32_HEX, bytes, PADDING_32);
+ }
+
+ public static boolean isBase32Hex(String data) {
+ return isEncodedByAlphabet(REV_BASE_32_HEX, data, PADDING_32);
+ }
+
+ public static boolean isBase32Crockford(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_32_CROCKFORD, bytes, PADDING_32);
+ }
+
+ public static boolean isBase32Crockford(String data) {
+ return isEncodedByAlphabet(REV_BASE_32_CROCKFORD, data, PADDING_32);
+ }
+
+ public static boolean isBase32Dmedia(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_32_DMEDIA, bytes, PADDING_32);
+ }
+
+ public static boolean isBase32Dmedia(String data) {
+ return isEncodedByAlphabet(REV_BASE_32_DMEDIA, data, PADDING_32);
+ }
+
+ public static boolean isBase16(byte[] bytes) {
+ return isEncodedByAlphabet(REV_BASE_16, bytes, 0);
+ }
+
+ public static boolean isBase16(String data) {
+ return isEncodedByAlphabet(REV_BASE_16, data, 0);
+ }
+
+ public static boolean isEncodedByAlphabet(byte[] alphabet, String data, int padding) {
+ return isEncodedByAlphabet(alphabet, data.getBytes(), padding);
+ }
+
+ /**
+ * @param alphabet
+ * @param bytes
+ * @param maxPadding
+ *
+ * @return
+ */
+ public static boolean isEncodedByAlphabet(byte[] alphabet, byte[] bytes, int maxPadding) {
+ if (bytes.length == 0)
+ return true;
+
+ int paddingStart = 0;
+ for (int i = 0; i < bytes.length; i++) {
+ byte b = bytes[i];
+ if (b < 0 || b > alphabet.length)
+ return false;
+
+ byte c = alphabet[b];
+ if (c == -1) {
+
+ if (b == PAD && maxPadding != 0) {
+ if (paddingStart == 0)
+ paddingStart = i;
+
+ continue;
+ }
+
+ return false;
+ }
+ }
+
+ if (paddingStart != 0 && paddingStart < (bytes.length - maxPadding))
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Encodes the given data to a 64-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet
+ *
+ * @param alphabet
+ * the 64-bit alphabet to use
+ * @param bytes
+ * the bytes to encode
+ *
+ * @return the encoded data
+ */
+ public static byte[] toBase64(byte[] alphabet, byte[] bytes) {
+ if (bytes.length == 0)
+ return new byte[0];
+ if (alphabet.length != 64) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 64 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ // 6 bits input for every 8 bits (1 byte) output
+ // least common multiple of 6 bits input and 8 bits output = 24
+ // and output multiple is then lcm(6, 8) / 6 = 4
+ // thus we need to write multiples of 4 bytes of data
+ int bitsIn = 6;
+ int outputMultiple = 4;
+
+ // first convert to bits
+ int nrOfInputBytes = bytes.length;
+ int nrOfInputBits = nrOfInputBytes * Byte.SIZE;
+
+ // calculate number of bits missing for multiples of bitsIn
+ int inputPadding = nrOfInputBits % bitsIn;
+ int nrOfOutputBytes;
+ if (inputPadding == 0)
+ nrOfOutputBytes = nrOfInputBits / bitsIn;
+ else
+ nrOfOutputBytes = (nrOfInputBits + (bitsIn - (inputPadding))) / bitsIn;
+
+ // calculate number of bits missing for multiple of bitsOut
+ int nrOfBytesPadding = outputMultiple - (nrOfOutputBytes % outputMultiple);
+ if (nrOfBytesPadding == outputMultiple)
+ nrOfBytesPadding = 0;
+
+ // actual result array is multiples of bitsOut/8 thus sum of:
+ int txtLength = nrOfOutputBytes + nrOfBytesPadding;
+
+// logger.info(String.format("Input: %d bytes, Output: %d bytes, Padding: %d bytes, TextLength: %d",
+// nrOfInputBytes, nrOfOutputBytes, nrOfBytesPadding, txtLength));
+
+ byte[] txt = new byte[txtLength];
+ long bits;
+ int bytesPos = 0;
+ int txtPos = 0;
+ while (bytesPos < bytes.length) {
+
+ int remaining = bytes.length - bytesPos;
+ // get up to 24 bits of data in 3 bytes
+ bits = 0;
+ if (remaining >= 3) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 16) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 8) //
+ | ((bytes[bytesPos++] & 0xff));
+
+ } else if (remaining == 2) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 16) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 8);
+
+ } else if (remaining == 1) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 16);
+ }
+
+ // always start at 24. bit
+ int bitPos = 23;
+
+ // always write 24 bits (6 bits * 4), but this will also write into the padding
+ // we will fix this by writing the padding as has been calculated previously
+ while (bitPos >= 0) {
+
+ int index = 0;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 32;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 16;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 8;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 4;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 2;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 1;
+ bitPos--;
+ byte character = alphabet[index];
+ txt[txtPos] = character;
+ txtPos++;
+ }
+ }
+
+ // write any padding that was calculated
+ if (nrOfBytesPadding != 0) {
+ int paddingPos = txtPos - nrOfBytesPadding;
+ for (; paddingPos < txtLength; paddingPos++) {
+ txt[paddingPos] = PAD;
+ }
+ }
+
+ return txt;
+ }
+
+ /**
+ * Encodes the given data to a 32-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet
+ *
+ * @param alphabet
+ * the 32-bit alphabet to use
+ * @param bytes
+ * the bytes to encode
+ *
+ * @return the encoded data
+ */
+ public static byte[] toBase32(byte[] alphabet, byte[] bytes) {
+ if (bytes.length == 0)
+ return new byte[0];
+ if (alphabet.length != 32) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 32 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ // 5 bits input for every 8 bits (1 byte) output
+ // least common multiple of 5 bits input and 8 bits output = 40
+ // and output multiple is then lcm(5, 8) / 5 = 8
+ // thus we need to write multiples of 8 bytes of data
+ int bitsIn = 5;
+ int outputMultiple = 8;
+
+ // first convert to bits
+ int nrOfInputBytes = bytes.length;
+ int nrOfInputBits = nrOfInputBytes * Byte.SIZE;
+
+ // calculate number of bits missing for multiples of bitsIn
+ int inputPadding = nrOfInputBits % bitsIn;
+ int nrOfOutputBytes;
+ if (inputPadding == 0)
+ nrOfOutputBytes = nrOfInputBits / bitsIn;
+ else
+ nrOfOutputBytes = (nrOfInputBits + (bitsIn - (inputPadding))) / bitsIn;
+
+ // calculate number of bits missing for multiple of bitsOut
+ int nrOfBytesPadding = outputMultiple - (nrOfOutputBytes % outputMultiple);
+ if (nrOfBytesPadding == outputMultiple)
+ nrOfBytesPadding = 0;
+
+ // actual result array is multiples of bitsOut/8 thus sum of:
+ int txtLength = nrOfOutputBytes + nrOfBytesPadding;
+
+// logger.info(String.format("Input: %d bytes, Output: %d bytes, Padding: %d bytes, TextLength: %d",
+// nrOfInputBytes, nrOfOutputBytes, nrOfBytesPadding, txtLength));
+
+ byte[] txt = new byte[txtLength];
+ long bits;
+ int bytesPos = 0;
+ int txtPos = 0;
+ while (bytesPos < bytes.length) {
+
+ int remaining = bytes.length - bytesPos;
+ // get up to 40 bits of data in 5 bytes
+ bits = 0;
+ if (remaining >= 5) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 32) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 24) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 16) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 8) //
+ | ((bytes[bytesPos++] & 0xff));
+
+ } else if (remaining == 4) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 32) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 24) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 16) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 8);
+
+ } else if (remaining == 3) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 32) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 24) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 16);
+
+ } else if (remaining == 2) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 32) //
+ | ((long) (bytes[bytesPos++] & 0xff) << 24);
+
+ } else if (remaining == 1) {
+
+ bits = ((long) (bytes[bytesPos++] & 0xff) << 32);
+
+ }
+
+ // always start at 40. bit
+ int bitPos = 39;
+
+ // always write 40 bits (5 bytes * 8 multiples), but this will also write into the padding
+ // we will fix this by writing the padding as has been calculated previously
+ while (bitPos >= 0) {
+
+ int index = 0;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 16;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 8;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 4;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 2;
+ bitPos--;
+ index |= ((bits >>> bitPos) & 1) == 0 ? 0 : 1;
+ bitPos--;
+ byte character = alphabet[index];
+ txt[txtPos] = character;
+ txtPos++;
+ }
+ }
+
+ // write any padding that was calculated
+ if (nrOfBytesPadding != 0) {
+ int paddingPos = txtPos - nrOfBytesPadding;
+ for (; paddingPos < txtLength; paddingPos++) {
+ txt[paddingPos] = PAD;
+ }
+ }
+
+ return txt;
+ }
+
+ /**
+ * Encodes the given data to a 16-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet
+ *
+ * @param alphabet
+ * the 16-bit alphabet to use
+ * @param bytes
+ * the bytes to encode
+ *
+ * @return the encoded data
+ */
+ public static byte[] toBase16(byte[] alphabet, byte[] bytes) {
+ if (bytes.length == 0)
+ return new byte[0];
+ if (alphabet.length != 16) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 16 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ // calculate output text length
+ int nrOfInputBytes = bytes.length;
+ int nrOfOutputBytes = nrOfInputBytes * 2;
+ int txtLength = nrOfOutputBytes;
+
+// logger.info(String.format("Input: %d bytes, Output: %d bytes, TextLength: %d", nrOfInputBytes, nrOfOutputBytes,
+// txtLength));
+
+ byte[] txt = new byte[txtLength];
+ byte bits;
+ int bytesPos = 0;
+ int txtPos = 0;
+ while (bytesPos < bytes.length) {
+
+ // get 8 bits of data (1 byte)
+ bits = bytes[bytesPos++];
+
+ // now write the 8 bits as 2 * 4 bits
+
+ // output byte 1
+ int index = (bits >>> 4) & 0xf;
+ byte character = alphabet[index];
+ txt[txtPos] = character;
+ txtPos++;
+
+ // output byte 2
+ index = bits & 0xf;
+ character = alphabet[index];
+ txt[txtPos] = character;
+ txtPos++;
+ }
+
+ return txt;
+ }
+
+ /**
+ * Decodes the given Base64 encoded data to the original data set
+ *
+ * @param alphabet
+ * the 64-bit alphabet to use
+ * @param bytes
+ * the bytes to decode
+ *
+ * @return the decoded data
+ */
+ public static byte[] fromBase64(byte[] alphabet, byte[] bytes) {
+ int inputLength = bytes.length;
+ if (inputLength == 0)
+ return new byte[0];
+ if ((inputLength % 4) != 0) {
+ String msg = MessageFormat.format(
+ "The input bytes to be decoded must be multiples of 4, but is multiple of {0}", //$NON-NLS-1$
+ (inputLength % 4));
+ throw new RuntimeException(msg);
+ }
+
+ if (alphabet.length != 128) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 128 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ if (!isEncodedByAlphabet(alphabet, bytes, PADDING_64))
+ throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); //$NON-NLS-1$
+
+ // find how much padding we have
+ int nrOfBytesPadding = 0;
+ if (bytes[inputLength - 1] == PAD) {
+ int end = inputLength - 1;
+ while (bytes[end] == PAD)
+ end--;
+ if (end != inputLength - 1)
+ nrOfBytesPadding = inputLength - 1 - end;
+ }
+
+ int inputDataLength = inputLength - nrOfBytesPadding;
+ int dataLengthBits = inputDataLength * 6; // 6 bits data for every 8 bits inputs
+ // multiples of 6 required
+ // truncating is no problem due to the input having padding to have multiples of 32 bits
+ dataLengthBits = dataLengthBits - (dataLengthBits % 8);
+ int dataLengthBytes = dataLengthBits / 8;
+
+ // f => Zg==
+ // fo => Zm8=
+ // foo => Zm9v
+
+ // we want to write as much as 24 bits in multiples of 6.
+ // these multiples of 6 are read from multiples of 8
+ // i.e. we discard 2 bits from every 8 bits input
+ // thus we need to read 24 / 6 = 4 bytes
+
+ byte[] data = new byte[dataLengthBytes];
+ int dataPos = 0;
+
+ // but we simply ignore the padding
+ int bytesPos = 0;
+ while (bytesPos < inputDataLength) {
+ int remaining = inputDataLength - bytesPos;
+
+ long bits;
+ if (remaining >= 4) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 63) << 18) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 63) << 12) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 63) << 6) //
+ | (alphabet[bytes[bytesPos++]] & 63);
+
+ } else if (remaining == 3) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 63) << 18) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 63) << 12) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 63) << 6);
+
+ } else if (remaining == 2) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 63) << 18) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 63) << 12);
+//
+// long b;
+// byte a;
+// a = bytes[0];
+// logger.info("1 a: " + a + " " + ((char) a) + " - " + ByteHelper.asBinary(a) + " " + indexOf(a));
+// b = (byte) (alphabet[a] & 63);
+// logger.info("1 b: " + b + " - " + ((char) b) + " - " + ByteHelper.asBinary(b));
+// a = bytes[1];
+// logger.info("2 a: " + a + " " + ((char) a) + " - " + ByteHelper.asBinary(a) + " " + indexOf(a));
+// b = (byte) (alphabet[a] & 63);
+// logger.info("2 b: " + b + " - " + ((char) b) + " - " + ByteHelper.asBinary(b));
+
+ } else if (remaining == 1) {
+
+ bits = ((alphabet[bytes[bytesPos++]] & 63) << 18);
+
+ } else {
+
+ bits = 0L;
+ }
+
+ // we can truncate to 8 bits
+ int toWrite = remaining >= 4 ? 3 : remaining * 6 / 8;
+ // max is always 3 bytes data from 4 bytes input
+
+// logger.info("toWrite: " + toWrite + ", remaining: " + remaining);
+// logger.info("bits: " + ByteHelper.asBinary(bits));
+
+ // always start at 24. bit
+ int bitPos = 23;
+ // always write 24 bits (8 bits * n bytes)
+ for (int i = 0; i < toWrite; i++) {
+
+ byte value = 0;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 128;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 64;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 32;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 16;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 8;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 4;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 2;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 1;
+ bitPos--;
+ data[dataPos] = value;
+ dataPos++;
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Decodes the given Base32 encoded data to the original data set
+ *
+ * @param alphabet
+ * the 32-bit alphabet to use
+ * @param bytes
+ * the bytes to decode
+ *
+ * @return the decoded data
+ */
+ public static byte[] fromBase32(byte[] alphabet, byte[] bytes) {
+ int inputLength = bytes.length;
+ if (inputLength == 0)
+ return new byte[0];
+ if ((inputLength % 8) != 0) {
+ String msg = "The input bytes to be decoded must be multiples of 8, but is multiple of {0}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, (inputLength % 8));
+ throw new RuntimeException(msg);
+ }
+
+ if (alphabet.length != 128) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 128 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ if (!isEncodedByAlphabet(alphabet, bytes, PADDING_32))
+ throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); //$NON-NLS-1$
+
+ // find how much padding we have
+ int nrOfBytesPadding = 0;
+ if (bytes[inputLength - 1] == PAD) {
+ int end = inputLength - 1;
+ while (bytes[end] == PAD)
+ end--;
+ if (end != inputLength - 1)
+ nrOfBytesPadding = inputLength - 1 - end;
+ }
+
+ int inputDataLength = inputLength - nrOfBytesPadding;
+ int dataLengthBits = inputDataLength * 5; // 5 bits data for every 8 bits inputs
+ // multiples of 8 required
+ // truncating is no problem due to the input having padding to have multiples of 40 bits
+ dataLengthBits = dataLengthBits - (dataLengthBits % 8);
+ int dataLengthBytes = dataLengthBits / 8;
+
+// logger.info("Input " + inputLength + " bytes, InputData " + inputDataLength + " bytes, Padding: "
+// + nrOfBytesPadding + " bytes, dataLength: " + dataLengthBits + " bits, dataLengthBytes: "
+// + dataLengthBytes + " bytes");
+// logger.info(ByteHelper.asBinary(bytes));
+
+ // we want to write as much as 40 bits in multiples of 5.
+ // these multiples of 5 are read from multiples of 8
+ // i.e. we discard 3 bits from every 8 bits input
+ // thus we need to read 40 / 5 = 8 bytes
+
+ byte[] data = new byte[dataLengthBytes];
+ int dataPos = 0;
+
+ // but we simply ignore the padding
+ int bytesPos = 0;
+ while (bytesPos < inputDataLength) {
+ int remaining = inputDataLength - bytesPos;
+
+ long bits;
+ if (remaining >= 8) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 20) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 15) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 10) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 5) //
+ | (alphabet[bytes[bytesPos++]] & 31);
+
+ } else if (remaining >= 7) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 20) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 15) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 10) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 5);
+
+ } else if (remaining == 6) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 20) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 15) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 10);
+
+ } else if (remaining == 5) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 20) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 15);
+
+ } else if (remaining == 4) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 20);
+
+ } else if (remaining == 3) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 25);
+
+ } else if (remaining == 2) {
+
+ bits = ((long) (alphabet[bytes[bytesPos++]] & 31) << 35) //
+ | ((long) (alphabet[bytes[bytesPos++]] & 31) << 30);
+
+ } else if (remaining == 1) {
+
+ bits = ((alphabet[bytes[bytesPos++]] & 31) << 35);
+
+ } else {
+
+ bits = 0L;
+ }
+
+ // we can truncate to 8 bits
+ int toRead = remaining >= 8 ? 5 : remaining * 5 / 8;
+ // max is always 5 bytes data from 8 bytes input
+
+ // always start at 40. bit
+ int bitPos = 39;
+ // always write 40 bits (5 bytes * 8 bits)
+ for (int i = 0; i < toRead; i++) {
+
+ byte value = 0;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 128;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 64;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 32;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 16;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 8;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 4;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 2;
+ bitPos--;
+ value |= ((bits >>> bitPos) & 1) == 0 ? 0 : 1;
+ bitPos--;
+ data[dataPos] = value;
+ dataPos++;
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Decodes the given Base16 encoded data to the original data set
+ *
+ * @param alphabet
+ * the 16-bit alphabet to use
+ * @param bytes
+ * the bytes to decode
+ *
+ * @return the decoded data
+ */
+ public static byte[] fromBase16(byte[] alphabet, byte[] bytes) {
+ if (bytes.length == 0)
+ return new byte[0];
+ if ((bytes.length % 2) != 0) {
+ String msg = "The input bytes to be decoded must be multiples of 4, but is multiple of {0}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, (bytes.length % 4));
+ throw new RuntimeException(msg);
+ }
+
+ if (alphabet.length != 128) {
+ String msg = MessageFormat.format("Alphabet does not have expected size 128 but is {0}", alphabet.length); //$NON-NLS-1$
+ throw new RuntimeException(msg);
+ }
+
+ if (!isEncodedByAlphabet(alphabet, bytes, 0))
+ throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); //$NON-NLS-1$
+
+ int dataLength = bytes.length / 2;
+
+ byte[] data = new byte[dataLength];
+ for (int i = 0; i < bytes.length;) {
+
+ byte b1 = bytes[i++];
+ byte b2 = bytes[i++];
+
+ String msgOutOfRange = "Value at index {0} is not in range of alphabet (0-127){1}"; //$NON-NLS-1$
+ if (b1 < 0) {
+ msgOutOfRange = MessageFormat.format(msgOutOfRange, (i - 2), b1);
+ throw new IllegalArgumentException(msgOutOfRange);
+ }
+ if (b2 < 0) {
+ msgOutOfRange = MessageFormat.format(msgOutOfRange, (i - 1), b2);
+ throw new IllegalArgumentException(msgOutOfRange);
+ }
+
+ byte c1 = alphabet[b1];
+ byte c2 = alphabet[b2];
+
+ String msgIllegalValue = "Value at index {0} is referencing illegal value in alphabet: {1}"; //$NON-NLS-1$
+ if (c1 == -1) {
+ msgIllegalValue = MessageFormat.format(msgIllegalValue, (i - 2), b1);
+ throw new IllegalArgumentException(msgIllegalValue);
+ }
+ if (c2 == -1) {
+ msgIllegalValue = MessageFormat.format(msgIllegalValue, (i - 2), b2);
+ throw new IllegalArgumentException(msgIllegalValue);
+ }
+
+ int dataIndex = (i / 2) - 1;
+ int value = ((c1 << 4) & 0xff) | c2;
+ data[dataIndex] = (byte) value;
+ }
+
+ return data;
+ }
+}
\ No newline at end of file
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java
new file mode 100644
index 000000000..97697ee28
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+/**
+ * @author Robert von Burg
+ */
+public class ByteHelper {
+
+ /**
+ * Creates a long of the given byte array. They byte array must be 8 bytes long. The byte at index 0 is the highest
+ * byte
+ *
+ * @param bytes
+ * the bytes to convert to a long
+ *
+ * @return the long created from the bytes
+ */
+ public static long toLong(byte[] bytes) {
+
+ if (bytes.length != 8)
+ throw new IllegalArgumentException("The input byte array for a long must have 8 values"); //$NON-NLS-1$
+
+ return ((long) (bytes[0] & 0xff) << 56) //
+ | ((long) (bytes[1] & 0xff) << 48) //
+ | ((long) (bytes[2] & 0xff) << 40) //
+ | ((long) (bytes[3] & 0xff) << 32) //
+ | ((long) (bytes[4] & 0xff) << 24) //
+ | ((long) (bytes[5] & 0xff) << 16) //
+ | ((long) (bytes[6] & 0xff) << 8) //
+ | ((bytes[7] & 0xff));
+ }
+
+ /**
+ * Creates an integer of the given byte array. They byte array must be 4 bytes long. The byte at index 0 is the
+ * highest byte
+ *
+ * @param bytes
+ * the bytes to convert to an integer
+ *
+ * @return the integer created from the bytes
+ */
+ public static int toInt(byte[] bytes) {
+
+ if (bytes.length != 4)
+ throw new IllegalArgumentException("The input byte array for a long must have 4 values"); //$NON-NLS-1$
+
+ return ((bytes[0] & 0xff) << 24) //
+ | ((bytes[1] & 0xff) << 16) //
+ | ((bytes[2] & 0xff) << 8) //
+ | ((bytes[3] & 0xff));
+ }
+
+ /**
+ * Formats the given byte to a binary string
+ *
+ * @param b
+ * the byte to format to a binary string
+ *
+ * @return the binary string
+ */
+ public static String asBinary(byte b) {
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(((b >>> 7) & 1));
+ sb.append(((b >>> 6) & 1));
+ sb.append(((b >>> 5) & 1));
+ sb.append(((b >>> 4) & 1));
+ sb.append(((b >>> 3) & 1));
+ sb.append(((b >>> 2) & 1));
+ sb.append(((b >>> 1) & 1));
+ sb.append(((b >>> 0) & 1));
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the given byte array to a binary string, separating each byte by a space
+ *
+ * @param bytes
+ * the byte to format to a binary string
+ *
+ * @return the binary string
+ */
+ public static String asBinary(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+
+ for (byte b : bytes) {
+ sb.append(asBinary(b));
+ sb.append(StringHelper.SPACE);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the given integer to a binary string, each byte is separated by a space
+ *
+ * @param i
+ * the integer to format to a string
+ *
+ * @return the binary string
+ */
+ public static String asBinary(int i) {
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(((i >>> 31) & 1));
+ sb.append(((i >>> 30) & 1));
+ sb.append(((i >>> 29) & 1));
+ sb.append(((i >>> 28) & 1));
+ sb.append(((i >>> 27) & 1));
+ sb.append(((i >>> 26) & 1));
+ sb.append(((i >>> 25) & 1));
+ sb.append(((i >>> 24) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 23) & 1));
+ sb.append(((i >>> 22) & 1));
+ sb.append(((i >>> 21) & 1));
+ sb.append(((i >>> 20) & 1));
+ sb.append(((i >>> 19) & 1));
+ sb.append(((i >>> 18) & 1));
+ sb.append(((i >>> 17) & 1));
+ sb.append(((i >>> 16) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 15) & 1));
+ sb.append(((i >>> 14) & 1));
+ sb.append(((i >>> 13) & 1));
+ sb.append(((i >>> 12) & 1));
+ sb.append(((i >>> 11) & 1));
+ sb.append(((i >>> 10) & 1));
+ sb.append(((i >>> 9) & 1));
+ sb.append(((i >>> 8) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 7) & 1));
+ sb.append(((i >>> 6) & 1));
+ sb.append(((i >>> 5) & 1));
+ sb.append(((i >>> 4) & 1));
+ sb.append(((i >>> 3) & 1));
+ sb.append(((i >>> 2) & 1));
+ sb.append(((i >>> 1) & 1));
+ sb.append(((i >>> 0) & 1));
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the given long to a binary string, each byte is separated by a space
+ *
+ * @param i
+ * the long to format
+ *
+ * @return the binary string
+ */
+ public static String asBinary(long i) {
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(((i >>> 63) & 1));
+ sb.append(((i >>> 62) & 1));
+ sb.append(((i >>> 61) & 1));
+ sb.append(((i >>> 60) & 1));
+ sb.append(((i >>> 59) & 1));
+ sb.append(((i >>> 58) & 1));
+ sb.append(((i >>> 57) & 1));
+ sb.append(((i >>> 56) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 55) & 1));
+ sb.append(((i >>> 54) & 1));
+ sb.append(((i >>> 53) & 1));
+ sb.append(((i >>> 52) & 1));
+ sb.append(((i >>> 51) & 1));
+ sb.append(((i >>> 50) & 1));
+ sb.append(((i >>> 49) & 1));
+ sb.append(((i >>> 48) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 47) & 1));
+ sb.append(((i >>> 46) & 1));
+ sb.append(((i >>> 45) & 1));
+ sb.append(((i >>> 44) & 1));
+ sb.append(((i >>> 43) & 1));
+ sb.append(((i >>> 42) & 1));
+ sb.append(((i >>> 41) & 1));
+ sb.append(((i >>> 40) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 39) & 1));
+ sb.append(((i >>> 38) & 1));
+ sb.append(((i >>> 37) & 1));
+ sb.append(((i >>> 36) & 1));
+ sb.append(((i >>> 35) & 1));
+ sb.append(((i >>> 34) & 1));
+ sb.append(((i >>> 33) & 1));
+ sb.append(((i >>> 32) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 31) & 1));
+ sb.append(((i >>> 30) & 1));
+ sb.append(((i >>> 29) & 1));
+ sb.append(((i >>> 28) & 1));
+ sb.append(((i >>> 27) & 1));
+ sb.append(((i >>> 26) & 1));
+ sb.append(((i >>> 25) & 1));
+ sb.append(((i >>> 24) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 23) & 1));
+ sb.append(((i >>> 22) & 1));
+ sb.append(((i >>> 21) & 1));
+ sb.append(((i >>> 20) & 1));
+ sb.append(((i >>> 19) & 1));
+ sb.append(((i >>> 18) & 1));
+ sb.append(((i >>> 17) & 1));
+ sb.append(((i >>> 16) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 15) & 1));
+ sb.append(((i >>> 14) & 1));
+ sb.append(((i >>> 13) & 1));
+ sb.append(((i >>> 12) & 1));
+ sb.append(((i >>> 11) & 1));
+ sb.append(((i >>> 10) & 1));
+ sb.append(((i >>> 9) & 1));
+ sb.append(((i >>> 8) & 1));
+
+ sb.append(StringHelper.SPACE);
+
+ sb.append(((i >>> 7) & 1));
+ sb.append(((i >>> 6) & 1));
+ sb.append(((i >>> 5) & 1));
+ sb.append(((i >>> 4) & 1));
+ sb.append(((i >>> 3) & 1));
+ sb.append(((i >>> 2) & 1));
+ sb.append(((i >>> 1) & 1));
+ sb.append(((i >>> 0) & 1));
+
+ return sb.toString();
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java
new file mode 100644
index 000000000..b24d3cd9f
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.text.MessageFormat;
+
+/**
+ * Utility class for working with {@link Class Classes}
+ *
+ * @author Robert von Burg
+ */
+public class ClassHelper {
+
+ /**
+ * Returns an instance of the class' name given by instantiating the class through an empty arguments constructor
+ *
+ * @param
+ * the type of the class to return
+ * @param className
+ * the name of a class to instantiate through an empty arguments constructor
+ *
+ * @return the newly instantiated object from the given class name
+ *
+ * @throws IllegalArgumentException
+ * if the class could not be instantiated
+ */
+ @SuppressWarnings("unchecked")
+ public static T instantiateClass(String className) throws IllegalArgumentException {
+ try {
+
+ Class clazz = (Class) Class.forName(className);
+
+ return clazz.getConstructor().newInstance();
+
+ } catch (Exception e) {
+ String msg = MessageFormat.format("The class {0} could not be instantiated: ", className); //$NON-NLS-1$
+ throw new IllegalArgumentException(msg, e);
+ }
+ }
+
+ /**
+ * Instantiates an object for the given {@link Class} using an empty arguments constructor
+ *
+ * @param
+ * the type of the class to return
+ * @param clazz
+ * the {@link Class} from which a new object is to be instantiated using an empty arguments constructor
+ *
+ * @return the newly instantiated object from the given {@link Class}
+ *
+ * @throws IllegalArgumentException
+ * if the {@link Class} could not be instantiated
+ */
+ public static T instantiateClass(Class clazz) throws IllegalArgumentException {
+ try {
+
+ return clazz.getConstructor().newInstance();
+
+ } catch (Exception e) {
+ String msg = MessageFormat.format("The class {0} could not be instantiated: ", clazz.getName()); //$NON-NLS-1$
+ throw new IllegalArgumentException(msg, e);
+ }
+ }
+
+ /**
+ * Loads the {@link Class} object for the given class name
+ *
+ * @param
+ * the type of {@link Class} to return
+ * @param className
+ * the name of the {@link Class} to load and return
+ *
+ * @return the {@link Class} object for the given class name
+ *
+ * @throws IllegalArgumentException
+ * if the class could not be instantiated
+ */
+ @SuppressWarnings("unchecked")
+ public static Class loadClass(String className) throws IllegalArgumentException {
+ try {
+
+ Class clazz = (Class) Class.forName(className);
+
+ return clazz;
+
+ } catch (Exception e) {
+ String msg = MessageFormat.format("The class {0} could not be instantiated: ", className); //$NON-NLS-1$
+ throw new IllegalArgumentException(msg, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/DomUtil.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/DomUtil.java
new file mode 100644
index 000000000..20851de57
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/DomUtil.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.text.MessageFormat;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * @author Robert von Burg
+ */
+public class DomUtil {
+
+ public static DocumentBuilder createDocumentBuilder() {
+ try {
+ DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
+ DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
+ return docBuilder;
+ } catch (ParserConfigurationException e) {
+ String msg = "No Xml Parser could be loaded: {0}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, e.getMessage());
+ throw new RuntimeException(msg, e);
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java
new file mode 100644
index 000000000..a498c20cb
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2015 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * @author Robert von Burg
+ */
+public class ExceptionHelper {
+
+ /**
+ *
+ * Returns a message for the given {@link Throwable}
+ *
+ *
+ *
+ * A {@link NullPointerException} only has null as the message so this methods returns the class name
+ * in such a case
+ *
+ * Returns a message for the given {@link Throwable}
+ *
+ *
+ *
+ * A {@link NullPointerException} only has null as the message so this methods returns the class name
+ * in such a case
+ *
+ *
+ * @param t
+ * @return
+ */
+ public static String getExceptionMessageWithCauses(Throwable t) {
+ if (t.getCause() == null)
+ return getExceptionMessage(t);
+
+ String root = getExceptionMessageWithCauses(t.getCause());
+ return getExceptionMessage(t) + "\n" + root;
+ }
+
+ /**
+ * Formats the given {@link Throwable}'s stack trace to a string
+ *
+ * @param t
+ * the throwable for which the stack trace is to be formatted to string
+ *
+ * @return a string representation of the given {@link Throwable}'s stack trace
+ */
+ public static String formatException(Throwable t) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ t.printStackTrace(writer);
+ return stringWriter.toString();
+ }
+
+ /**
+ * Formats the given {@link Throwable}'s message including causes to a string
+ *
+ * @param t
+ * the throwable for which the messages are to be formatted to a string
+ *
+ * @return a string representation of the given {@link Throwable}'s messages including causes
+ */
+ public static String formatExceptionMessage(Throwable t) {
+ if (t.getCause() == null)
+ return getExceptionMessage(t);
+
+ String root = formatExceptionMessage(t.getCause());
+ return getExceptionMessage(t) + "\ncause:\n" + root;
+ }
+
+ /**
+ * Returns the root cause for the given {@link Throwable}
+ *
+ * @param throwable
+ * the {@link Throwable} for which to get the root cause
+ *
+ * @return the root cause of the given {@link Throwable}
+ */
+ public static Throwable getRootCause(Throwable throwable) {
+ Throwable t = throwable;
+ while (t.getCause() != null) {
+ t = t.getCause();
+ }
+
+ return t;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/FileHelper.java
new file mode 100644
index 000000000..57cf58bc9
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/FileHelper.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.MessageDigest;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helper class for dealing with files
+ *
+ * @author Robert von Burg
+ */
+public class FileHelper {
+
+ private static final int MAX_FILE_SIZE = 50 * 1024 * 1024;
+ private static final Logger logger = LoggerFactory.getLogger(FileHelper.class);
+
+ /**
+ * Reads the contents of a file into a byte array.
+ *
+ * @param file
+ * the file to read
+ *
+ * @return the contents of a file as a string
+ */
+ public static final byte[] readFile(File file) {
+ if (file.length() > MAX_FILE_SIZE)
+ throw new RuntimeException(String.format("Only allowed to read files up to %s. File too large: %s", //$NON-NLS-1$
+ humanizeFileSize(MAX_FILE_SIZE), humanizeFileSize(file.length())));
+
+ byte[] data = new byte[(int) file.length()];
+ int pos = 0;
+
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));) {
+ byte[] bytes = new byte[8192];
+ int read;
+ while ((read = in.read(bytes)) != -1) {
+ System.arraycopy(bytes, 0, data, pos, read);
+ pos += read;
+ }
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); //$NON-NLS-1$
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$
+ }
+
+ return data;
+ }
+
+ /**
+ * Reads the contents of a file into a string. Note, no encoding is checked. It is expected to be UTF-8
+ *
+ * @param file
+ * the file to read
+ *
+ * @return the contents of a file as a string
+ */
+ public static final String readFileToString(File file) {
+
+ try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file));) {
+
+ StringBuilder sb = new StringBuilder();
+
+ String line;
+
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line + "\n"); //$NON-NLS-1$
+ }
+
+ return sb.toString();
+
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("File does not exist " + file.getAbsolutePath()); //$NON-NLS-1$
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Reads the contents of a {@link InputStream} into a string. Note, no encoding is checked. It is expected to be
+ * UTF-8
+ *
+ * @param stream
+ * the stream to read
+ *
+ * @return the contents of a file as a string
+ */
+ public static final String readStreamToString(InputStream stream) {
+
+ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream));) {
+
+ StringBuilder sb = new StringBuilder();
+
+ String line;
+
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(line + "\n"); //$NON-NLS-1$
+ }
+
+ return sb.toString();
+
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read strean " + stream); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Writes the given byte array to the given file
+ *
+ * @param bytes
+ * the data to write to the file
+ * @param dstFile
+ * the path to which to write the data
+ */
+ public static final void writeToFile(byte[] bytes, File dstFile) {
+
+ try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dstFile));) {
+ out.write(bytes);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); //$NON-NLS-1$
+ } catch (IOException e) {
+ throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Writes the string to dstFile
+ *
+ * @param string
+ * string to write to file
+ * @param dstFile
+ * the file to write to
+ */
+ public static final void writeStringToFile(String string, File dstFile) {
+
+ try (BufferedWriter bufferedwriter = new BufferedWriter(new FileWriter(dstFile));) {
+ bufferedwriter.write(string);
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); //$NON-NLS-1$
+ } catch (IOException e) {
+ throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Deletes files recursively. No question asked, but logging is done in case of problems
+ *
+ * @param file
+ * the file to delete
+ * @param log
+ * @return true if all went well, and false if it did not work. The log will contain the problems encountered
+ */
+ public final static boolean deleteFile(File file, boolean log) {
+ return FileHelper.deleteFiles(new File[] { file }, log);
+ }
+
+ /**
+ * Deletes files recursively. No question asked, but logging is done in case of problems
+ *
+ * @param files
+ * the files to delete
+ * @param log
+ * @return true if all went well, and false if it did not work. The log will contain the problems encountered
+ */
+ public final static boolean deleteFiles(File[] files, boolean log) {
+
+ boolean worked = true;
+ for (int i = 0; i < files.length; i++) {
+ File file = files[i];
+ if (file.isDirectory()) {
+ boolean done = FileHelper.deleteFiles(file.listFiles(), log);
+ if (!done) {
+ worked = false;
+ FileHelper.logger.warn("Could not empty the directory: " + file.getAbsolutePath()); //$NON-NLS-1$
+ } else {
+ done = file.delete();
+ if (done) {
+ if (log)
+ FileHelper.logger.info("Deleted DIR " + file.getAbsolutePath()); //$NON-NLS-1$
+ } else {
+ worked = false;
+ FileHelper.logger.warn("Could not delete the directory: " + file.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+ } else {
+ boolean done = file.delete();
+ if (done) {
+ if (log)
+ FileHelper.logger.info("Deleted FILE " + file.getAbsolutePath()); //$NON-NLS-1$
+ } else {
+ worked = false;
+ FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath())); //$NON-NLS-1$
+ }
+ }
+ }
+ return worked;
+ }
+
+ /**
+ *
+ * Copy a given list of {@link File Files}. Recursively copies the files and directories to the destination.
+ *
+ *
+ * @param srcFiles
+ * The source files to copy
+ * @param dstDirectory
+ * The destination where to copy the files
+ * @param checksum
+ * if true, then a MD5 checksum is made to validate copying
+ * @return true if and only if the copying succeeded; false otherwise
+ */
+ public final static boolean copy(File[] srcFiles, File dstDirectory, boolean checksum) {
+
+ if (!dstDirectory.isDirectory() || !dstDirectory.canWrite()) {
+ String msg = "Destination is not a directory or is not writeable: {0}"; //$NON-NLS-1$
+ throw new IllegalArgumentException(MessageFormat.format(msg, dstDirectory.getAbsolutePath()));
+ }
+
+ for (File srcFile : srcFiles) {
+
+ File dstFile = new File(dstDirectory, srcFile.getName());
+ if (srcFile.isDirectory()) {
+ dstFile.mkdir();
+ if (!copy(srcFile.listFiles(), dstFile, checksum)) {
+ String msg = "Failed to copy contents of {0} to {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, srcFile.getAbsolutePath(), dstFile.getAbsolutePath());
+ logger.error(msg);
+ return false;
+ }
+ } else {
+ if (!copy(srcFile, dstFile, checksum)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Copy a {@link File} The renameTo method does not allow action across NFS mounted filesystems this method is the
+ * workaround
+ *
+ * @param fromFile
+ * The existing File
+ * @param toFile
+ * The new File
+ * @param checksum
+ * if true, then a MD5 checksum is made to validate copying
+ * @return true if and only if the renaming succeeded; false otherwise
+ */
+ public final static boolean copy(File fromFile, File toFile, boolean checksum) {
+
+ try (BufferedInputStream inBuffer = new BufferedInputStream(new FileInputStream(fromFile));
+ BufferedOutputStream outBuffer = new BufferedOutputStream(new FileOutputStream(toFile));) {
+
+ int theByte = 0;
+
+ while ((theByte = inBuffer.read()) > -1) {
+ outBuffer.write(theByte);
+ }
+
+ outBuffer.flush();
+
+ if (checksum) {
+ String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile));
+ String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile));
+ if (!fromFileMD5.equals(toFileMD5)) {
+ FileHelper.logger.error(MessageFormat.format(
+ "Copying failed, as MD5 sums are not equal: {0} / {1}", fromFileMD5, toFileMD5)); //$NON-NLS-1$
+ toFile.delete();
+
+ return false;
+ }
+ }
+
+ // cleanup if files are not the same length
+ if (fromFile.length() != toFile.length()) {
+ String msg = "Copying failed, as new files are not the same length: {0} / {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, fromFile.length(), toFile.length());
+ FileHelper.logger.error(msg);
+ toFile.delete();
+
+ return false;
+ }
+
+ } catch (Exception e) {
+ String msg = MessageFormat.format("Failed to copy path from {0} to + {1} due to:", fromFile, toFile); //$NON-NLS-1$
+ FileHelper.logger.error(msg, e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Move a File The renameTo method does not allow action across NFS mounted filesystems this method is the
+ * workaround
+ *
+ * @param fromFile
+ * The existing File
+ * @param toFile
+ * The new File
+ * @return true if and only if the renaming succeeded; false otherwise
+ */
+ public final static boolean move(File fromFile, File toFile) {
+
+ if (fromFile.renameTo(toFile)) {
+ return true;
+ }
+
+ FileHelper.logger.warn("Simple File.renameTo failed, trying copy/delete..."); //$NON-NLS-1$
+
+ // delete if copy was successful, otherwise move will fail
+ if (FileHelper.copy(fromFile, toFile, true)) {
+ FileHelper.logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); //$NON-NLS-1$
+ return fromFile.delete();
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds the common parent for the files in the given list
+ *
+ * @param files
+ * the files to find the common parent for
+ *
+ * @return the {@link File} representing the common parent, or null if there is none
+ */
+ public static File findCommonParent(List files) {
+
+ // be gentle with bad input data
+ if (files.size() == 0)
+ return null;
+ if (files.size() == 1)
+ return files.get(0).getParentFile();
+
+ File commonParent = null;
+ int commonParentDepth = -1;
+
+ // find the common parent among all the files
+ for (int i = 0; i < files.size() - 1; i++) {
+
+ // get first file
+ File file = files.get(i);
+
+ // get list of parents for this file
+ List parents = new ArrayList<>();
+ File parent = file.getParentFile();
+ while (parent != null) {
+ parents.add(parent);
+ parent = parent.getParentFile();
+ }
+ // reverse
+ Collections.reverse(parents);
+
+ // and now the same for the next file
+ File fileNext = files.get(i + 1);
+ List parentsNext = new ArrayList<>();
+ File parentNext = fileNext.getParentFile();
+ while (parentNext != null) {
+ parentsNext.add(parentNext);
+ parentNext = parentNext.getParentFile();
+ }
+ // reverse
+ Collections.reverse(parentsNext);
+
+ //logger.info("Files: " + file + " / " + fileNext);
+
+ // now find the common parent
+ File newCommonParent = null;
+ int newCommonParentDepth = -1;
+ for (int j = 0; j < (parents.size()); j++) {
+
+ // don't overflow the size of the next list of parents
+ if (j >= parentsNext.size())
+ break;
+
+ // if we once found a common parent, and our current depth is
+ // greater than the one for the common parent, then stop as
+ // there can't be a deeper parent
+ if (commonParent != null && j > commonParentDepth)
+ break;
+
+ // get the next parents to compare
+ File aParent = parents.get(j);
+ File bParent = parentsNext.get(j);
+
+ //logger.info("Comparing " + aParent + " |||| " + bParent);
+
+ // if they parent are the same, then break, as we won't
+ // have another match
+ if (!aParent.equals(bParent))
+ break;
+
+ // save the parent and the depth where we found the parent
+ newCommonParent = aParent;
+ newCommonParentDepth = j;
+ }
+
+ // if no common parent was found, then break as there won't be one
+ if (commonParent == null && newCommonParent == null)
+ break;
+
+ // if there is no new common parent, then check the next file
+ if (newCommonParent == null)
+ continue;
+
+ // store the common parent
+ commonParent = newCommonParent;
+ commonParentDepth = newCommonParentDepth;
+ //logger.info("Temporary common parent: (" + commonParentDepth + ") " + commonParent);
+ }
+
+ //logger.info("Common parent: " + commonParent);
+ return commonParent;
+ }
+
+ /**
+ * Returns the size of the file in a human readable form. Everything smaller than 1024 bytes is returned as x bytes,
+ * next is KB, then MB and then GB
+ *
+ * @param file
+ * the file for which the humanized size is to be returned
+ *
+ * @return the humanized form of the files size
+ */
+ public final static String humanizeFileSize(File file) {
+ return FileHelper.humanizeFileSize(file.length());
+ }
+
+ /**
+ * Returns the size of the file in a human readable form. Everything smaller than 1024 bytes is returned as x bytes,
+ * next is KB, then MB and then GB
+ *
+ * @param fileSize
+ * the size of a file for which the humanized size is to be returned
+ *
+ * @return the humanized form of the files size
+ */
+ @SuppressWarnings("nls")
+ public final static String humanizeFileSize(long fileSize) {
+ if (fileSize < 1024)
+ return String.format("%d bytes", fileSize);
+
+ if (fileSize < 1048576)
+ return String.format("%.1f KB", (fileSize / 1024.0d));
+
+ if (fileSize < 1073741824)
+ return String.format("%.1f MB", (fileSize / 1048576.0d));
+
+ return String.format("%.1f GB", (fileSize / 1073741824.0d));
+ }
+
+ /**
+ * Creates the MD5 hash of the given file, returning the hash as a byte array. Use
+ * {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
+ *
+ * @param file
+ * the file to hash
+ *
+ * @return the hash as a byte array
+ */
+ public static byte[] hashFileMd5(File file) {
+ return FileHelper.hashFile(file, "MD5"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the SHA1 hash of the given file, returning the hash as a byte array. Use
+ * {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
+ *
+ * @param file
+ * the file to hash
+ *
+ * @return the hash as a byte array
+ */
+ public static byte[] hashFileSha1(File file) {
+ return FileHelper.hashFile(file, "SHA-1"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the SHA256 hash of the given file, returning the hash as a byte array. Use
+ * {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
+ *
+ * @param file
+ * the file to hash
+ *
+ * @return the hash as a byte array
+ */
+ public static byte[] hashFileSha256(File file) {
+ return FileHelper.hashFile(file, "SHA-256"); //$NON-NLS-1$
+ }
+
+ /**
+ * Creates the hash of the given file with the given algorithm, returning the hash as a byte array. Use
+ * {@link StringHelper#getHexString(byte[])} to create a HEX string of the bytes
+ *
+ * @param file
+ * the file to hash
+ * @param algorithm
+ * the hashing algorithm to use
+ *
+ * @return the hash as a byte array
+ */
+ public static byte[] hashFile(File file, String algorithm) {
+ try (InputStream fis = new FileInputStream(file);) {
+
+ byte[] buffer = new byte[1024];
+ MessageDigest complete = MessageDigest.getInstance(algorithm);
+ int numRead;
+ do {
+ numRead = fis.read(buffer);
+ if (numRead > 0) {
+ complete.update(buffer, 0, numRead);
+ }
+ } while (numRead != -1);
+
+ return complete.digest();
+ } catch (Exception e) {
+ throw new RuntimeException("Something went wrong while hashing file: " + file.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Helper method to append bytes to a specified file. The file is created if it does not exist otherwise the bytes
+ * are simply appended
+ *
+ * @param dstFile
+ * the file to append to
+ * @param bytes
+ * the bytes to append
+ */
+ public static void appendFilePart(File dstFile, byte[] bytes) {
+
+ try (FileOutputStream outputStream = new FileOutputStream(dstFile, true);) {
+
+ outputStream.write(bytes);
+ outputStream.flush();
+
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create and append the bytes to the file " + dstFile.getAbsolutePath()); //$NON-NLS-1$
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/MathHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/MathHelper.java
new file mode 100644
index 000000000..6c868b889
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/MathHelper.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2013 Martin Smock
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * A helper class that contains mathematical computations that can be used throughout.
+ *
+ * @author Martin Smock
+ * @author Michael Gatto
+ */
+public class MathHelper {
+
+ public static final double PRECISION = 1.0E08;
+ public static final int PRECISION_DIGITS = 3;
+
+ /**
+ * Check if the two values are equal with respect to the precision
+ *
+ * @param firstValue
+ * the first value to compare
+ * @param secondValue
+ * the second value to compare to
+ * @return boolean True, if the two values are equal under the set precision. Fales, otherwise.
+ */
+ public static boolean isEqualPrecision(double firstValue, double secondValue) {
+
+ return (java.lang.Math.abs(firstValue - secondValue) < (1.0d / PRECISION));
+ }
+
+ /**
+ * Comparison between the two values. Given the precision, this function determines if the given value is smaller
+ * than the given bound.
+ *
+ * Note: this implementation tests if the value < bound, and if this is not so, checks if the values are equal under
+ * the precision. Thus, it's efficient whenever the value is expected to be smaller than the bound.
+ *
+ * @param value
+ * The value to check
+ * @param bound
+ * The bound given
+ * @return true, if value < bound under the given precision. False, otherwise.
+ */
+ public static boolean isSmallerEqualPrecision(double value, double bound) {
+ if (value < bound)
+ return true;
+ return isEqualPrecision(value, bound);
+ }
+
+ /**
+ * Comparison between two values. Given the precision, this function determines if the given value is greater than
+ * the given bound.
+ *
+ * Note: This implementation tests if value > bound and, if it is so, if equality does NOT hold. Thus, it is
+ * efficient whenever the value is not expected to be greater than the bound.
+ *
+ * @param value
+ * The value to check
+ * @param bound
+ * The bound given.
+ * @return true, if value > bound and the values do not test equal under precision. False, otherwise.
+ */
+ public static boolean isGreaterPrecision(double value, double bound) {
+ return (value > bound) && !isEqualPrecision(value, bound);
+ }
+
+ /**
+ *
+ * Rounds the given double value to the number of decimals
+ *
+ *
+ *
+ * Warning: Do not use the returned value for further calculations. Always finish calculates and use one of
+ * the following methods:
+ *
+ *
{@link #isEqualPrecision(double, double)},
+ *
{@link #isGreaterPrecision(double, double)} or
+ *
{@link #isSmallerEqualPrecision(double, double)}
+ *
+ * to test on equality or greater than/ smaller than
+ *
+ *
+ * @param value
+ * the double value to round
+ * @param decimals
+ * number of decimals
+ *
+ * @return the rounded number
+ */
+ public static double toPrecision(double value, int decimals) {
+ if (value == 0.0)
+ return 0.0;
+ if (Double.isNaN(value))
+ return Double.NaN;
+ if (value == Double.NEGATIVE_INFINITY)
+ return Double.NEGATIVE_INFINITY;
+ if (value == Double.POSITIVE_INFINITY)
+ return Double.POSITIVE_INFINITY;
+ return new BigDecimal(value).setScale(decimals, RoundingMode.HALF_EVEN).doubleValue();
+ }
+
+ /**
+ * Returns the value with the precision where precision is set to {@link #PRECISION_DIGITS}
+ *
+ * @see #toPrecision(double, int)
+ */
+ public static double toPrecision(double value) {
+ return toPrecision(value, PRECISION_DIGITS);
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java
new file mode 100644
index 000000000..2d84b0624
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Robert von Burg
+ */
+public class ProcessHelper {
+
+ private static final Logger logger = LoggerFactory.getLogger(ProcessHelper.class);
+
+ public static ProcessResult runCommand(String... commandAndArgs) {
+ return runCommand(null, commandAndArgs);
+ }
+
+ public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) {
+ return runCommand(30, TimeUnit.SECONDS, workingDirectory, commandAndArgs);
+ }
+
+ public static ProcessResult runCommand(long timeout, TimeUnit unit, String... commandAndArgs) {
+ return runCommand(timeout, unit, null, commandAndArgs);
+ }
+
+ public static ProcessResult runCommand(long timeout, TimeUnit unit, File workingDirectory,
+ String... commandAndArgs) {
+
+ if (workingDirectory != null && !workingDirectory.isDirectory()) {
+ String msg = "Working directory does not exist or is not a directory at {0}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, workingDirectory.getAbsolutePath());
+ throw new RuntimeException(msg);
+ }
+ if (commandAndArgs == null || commandAndArgs.length == 0)
+ throw new RuntimeException("No command passed!"); //$NON-NLS-1$
+
+ final StringBuffer sb = new StringBuffer();
+ sb.append("=====================================\n"); //$NON-NLS-1$
+ try {
+
+ ProcessBuilder pb = new ProcessBuilder(commandAndArgs);
+ pb.environment();
+ pb.directory(workingDirectory);
+
+ long start = System.nanoTime();
+ logger.info(MessageFormat.format("Starting command (Timeout {0} {1}) {2}", timeout, unit.name(),
+ Arrays.stream(commandAndArgs).collect(Collectors.joining(" "))));
+ final Process process = pb.start();
+ int[] returnValue = new int[1];
+
+ try (BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+ BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
+
+ Thread errorIn = new Thread(() -> readStream(sb, "[ERROR] ", errorStream), "errorIn");
+ errorIn.start();
+
+ Thread infoIn = new Thread(() -> readStream(sb, "[INFO] ", inputStream), "infoIn");
+ infoIn.start();
+
+ boolean ok = process.waitFor(timeout, unit);
+ if (!ok)
+ logger.error("Command failed to end before timeout or failed to execute.");
+
+ if (!process.isAlive()) {
+ returnValue[0] = process.exitValue();
+ } else {
+ logger.error("Forcibly destroying as still running...");
+ process.destroyForcibly();
+ process.waitFor(5, TimeUnit.SECONDS);
+ returnValue[0] = -1;
+ }
+
+ errorIn.join(100l);
+ infoIn.join(100l);
+ sb.append("=====================================\n"); //$NON-NLS-1$
+ }
+
+ logger.info("Command ended after " + StringHelper.formatNanoDuration(System.nanoTime() - start));
+ return new ProcessResult(returnValue[0], sb.toString(), null);
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); //$NON-NLS-1$
+ } catch (InterruptedException e) {
+ logger.error("Interrupted!"); //$NON-NLS-1$
+ sb.append("[FATAL] Interrupted"); //$NON-NLS-1$
+ return new ProcessResult(-1, sb.toString(), e);
+ }
+ }
+
+ public static class ProcessResult {
+ public final int returnValue;
+ public final String processOutput;
+ public final Throwable throwable;
+
+ public ProcessResult(int returnValue, String processOutput, Throwable t) {
+ this.returnValue = returnValue;
+ this.processOutput = processOutput;
+ this.throwable = t;
+ }
+ }
+
+ static void readStream(StringBuffer sb, String prefix, BufferedReader bufferedReader) {
+ String line;
+ try {
+ while ((line = bufferedReader.readLine()) != null) {
+ sb.append(prefix + line + StringHelper.NEW_LINE);
+ }
+ } catch (IOException e) {
+ String msg = "Faild to read from {0} stream: {1}"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, prefix, e.getMessage());
+ sb.append("[FATAL] "); //$NON-NLS-1$
+ sb.append(msg);
+ sb.append(StringHelper.NEW_LINE);
+ }
+ }
+
+ public static void openFile(File pdfPath) {
+
+ ProcessResult processResult;
+ if (SystemHelper.isLinux()) {
+ processResult = runCommand("xdg-open " + pdfPath.getAbsolutePath()); //$NON-NLS-1$
+ } else if (SystemHelper.isMacOS()) {
+ processResult = runCommand("open " + pdfPath.getAbsolutePath()); //$NON-NLS-1$
+ } else if (SystemHelper.isWindows()) {
+ // remove the first char (/) from the report path (/D:/temp.....)
+ String pdfFile = pdfPath.getAbsolutePath();
+ if (pdfFile.charAt(0) == '/')
+ pdfFile = pdfFile.substring(1);
+ processResult = runCommand("rundll32 url.dll,FileProtocolHandler " + pdfFile); //$NON-NLS-1$
+ } else {
+ String msg = MessageFormat.format("Unexpected OS: {0}", SystemHelper.osName); //$NON-NLS-1$
+ throw new UnsupportedOperationException(msg);
+ }
+
+ logProcessResult(processResult);
+ }
+
+ public static void logProcessResult(ProcessResult processResult) {
+ if (processResult.returnValue == 0) {
+ logger.info("Process executed successfully"); //$NON-NLS-1$
+ } else if (processResult.returnValue == -1) {
+ logger.error("Process execution failed:\n" + processResult.processOutput); //$NON-NLS-1$
+ logger.error(processResult.throwable.getMessage(), processResult.throwable);
+ } else {
+ String msg = "Process execution was not successful with return value:{0}\n{1}"; //$NON-NLS-1$
+ logger.info(MessageFormat.format(msg, processResult.returnValue, processResult.processOutput));
+ }
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java
new file mode 100644
index 000000000..f09f66e6c
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.text.MessageFormat;
+import java.util.Properties;
+
+/**
+ * @author Robert von Burg
+ */
+public class PropertiesHelper {
+
+ /**
+ * Returns the property with the given key from the given {@link Properties}. If def is null, and the property is
+ * not set, then a {@link RuntimeException} is thrown
+ *
+ * @param properties
+ * the {@link Properties} from which to retrieve the property
+ * @param context
+ * The context should be the name of the caller, so that stack trace gives enough detail as to which
+ * class expected the configuration property if no property was found
+ * @param key
+ * the key of the property to return
+ * @param def
+ * the default value, if null, then an exception will be thrown if the property is not set
+ *
+ * @return the property under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static String getProperty(Properties properties, String context, String key, String def)
+ throws RuntimeException {
+ String property = properties.getProperty(key, def);
+ if (property == null) {
+ String msg = "[{0}] Property {1} is not set, and no default was given!"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, context, key);
+ throw new RuntimeException(msg);
+ }
+
+ return property;
+ }
+
+ /**
+ * Returns the {@link System#getProperty(String)} with the given key. If def is null, and the property is not set,
+ * then a {@link RuntimeException} is thrown
+ *
+ * @param context
+ * The context should be the name of the caller, so that stack trace gives enough detail as to which
+ * class expected the configuration property if no property was found
+ * @param key
+ * the key of the property to return
+ * @param def
+ * the default value, if null, then an exception will be thrown if the property is not set
+ *
+ * @return the property under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static String getProperty(String context, String key, String def) throws RuntimeException {
+ return getProperty(System.getProperties(), context, key, def);
+ }
+
+ /**
+ * Delegates to {@link #getProperty(Properties, String, String, String)} but returns the value as a {@link Boolean}
+ * where {@link Boolean#valueOf(String)} defines the actual value
+ *
+ * @param properties
+ * the {@link Properties} from which to retrieve the property
+ * @return the property as a {@link Boolean} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Boolean getPropertyBool(Properties properties, String context, String key, Boolean def)
+ throws RuntimeException {
+ return Boolean.valueOf(getProperty(properties, context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(String, String, String)} but returns the value as a {@link Boolean} where
+ * {@link Boolean#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Boolean} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Boolean getPropertyBool(String context, String key, Boolean def) throws RuntimeException {
+ return Boolean.valueOf(getProperty(context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(Properties, String, String, String)} but returns the value as an {@link Integer}
+ * where {@link Integer#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Integer} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Integer getPropertyInt(Properties properties, String context, String key, Integer def)
+ throws RuntimeException {
+ return Integer.valueOf(getProperty(properties, context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(String, String, String)} but returns the value as an {@link Integer} where
+ * {@link Integer#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Integer} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Integer getPropertyInt(String context, String key, Integer def) throws RuntimeException {
+ return Integer.valueOf(getProperty(context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(Properties, String, String, String)} but returns the value as an {@link Double}
+ * where {@link Double#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Double} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Double getPropertyDouble(Properties properties, String context, String key, Double def)
+ throws RuntimeException {
+ return Double.valueOf(getProperty(properties, context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(String, String, String)} but returns the value as an {@link Double} where
+ * {@link Double#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Double} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Double getPropertyDouble(String context, String key, Double def) throws RuntimeException {
+ return Double.valueOf(getProperty(context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(Properties, String, String, String)} but returns the value as an {@link Long}
+ * where {@link Long#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Long} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Long getPropertyLong(Properties properties, String context, String key, Long def)
+ throws RuntimeException {
+ return Long.valueOf(getProperty(properties, context, key, def == null ? null : def.toString()));
+ }
+
+ /**
+ * Delegates to {@link #getProperty(String, String, String)} but returns the value as an {@link Long} where
+ * {@link Long#valueOf(String)} defines the actual value
+ *
+ * @return the property as a {@link Long} under the given key
+ *
+ * @throws RuntimeException
+ * if the property is not set and def is null
+ */
+ public static Long getPropertyLong(String context, String key, Long def) throws RuntimeException {
+ return Long.valueOf(getProperty(context, key, def == null ? null : def.toString()));
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/StringHelper.java
new file mode 100644
index 000000000..a7a3fcc94
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/StringHelper.java
@@ -0,0 +1,717 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.MessageFormat;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A helper class to perform different actions on {@link String}s
+ *
+ * @author Robert von Burg
+ */
+public class StringHelper {
+
+ public static final String NEW_LINE = "\n"; //$NON-NLS-1$
+ public static final String EMPTY = ""; //$NON-NLS-1$
+ public static final String SPACE = " "; //$NON-NLS-1$
+ public static final String NULL = "null"; //$NON-NLS-1$
+ public static final String DASH = "-"; //$NON-NLS-1$
+ public static final String UNDERLINE = "_"; //$NON-NLS-1$
+ public static final String COMMA = ","; //$NON-NLS-1$
+ public static final String DOT = "."; //$NON-NLS-1$
+ public static final String SEMICOLON = ";"; //$NON-NLS-1$
+ public static final String COLON = ":"; //$NON-NLS-1$
+
+ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class);
+
+ /**
+ * the semi-unique id which is incremented on every {@link #getUniqueId()}-method call
+ */
+ private static long uniqueId = System.currentTimeMillis() - 1119953500000l;
+
+ /**
+ * Hex char table for fast calculating of hex values
+ */
+ private static final byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4',
+ (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd',
+ (byte) 'e', (byte) 'f' };
+
+ /**
+ * Converts each byte of the given byte array to a HEX value and returns the concatenation of these values
+ *
+ * @param raw
+ * the bytes to convert to String using numbers in hexadecimal
+ *
+ * @return the encoded string
+ *
+ * @throws RuntimeException
+ */
+ public static String getHexString(byte[] raw) throws RuntimeException {
+ try {
+ byte[] hex = new byte[2 * raw.length];
+ int index = 0;
+
+ for (byte b : raw) {
+ int v = b & 0xFF;
+ hex[index++] = HEX_CHAR_TABLE[v >>> 4];
+ hex[index++] = HEX_CHAR_TABLE[v & 0xF];
+ }
+
+ return new String(hex, "ASCII"); //$NON-NLS-1$
+
+ } catch (UnsupportedEncodingException e) {
+ String msg = MessageFormat.format("Something went wrong while converting to HEX: {0}", e.getMessage()); //$NON-NLS-1$
+ throw new RuntimeException(msg, e);
+ }
+ }
+
+ /**
+ * Returns a byte array of a given string by converting each character of the string to a number base 16
+ *
+ * @param encoded
+ * the string to convert to a byt string
+ *
+ * @return the encoded byte stream
+ */
+ public static byte[] fromHexString(String encoded) {
+ if ((encoded.length() % 2) != 0)
+ throw new IllegalArgumentException("Input string must contain an even number of characters."); //$NON-NLS-1$
+
+ final byte result[] = new byte[encoded.length() / 2];
+ final char enc[] = encoded.toCharArray();
+ for (int i = 0; i < enc.length; i += 2) {
+ StringBuilder curr = new StringBuilder(2);
+ curr.append(enc[i]).append(enc[i + 1]);
+ result[i / 2] = (byte) Integer.parseInt(curr.toString(), 16);
+ }
+
+ return result;
+ }
+
+ /**
+ * Generates the MD5 Hash of a string and converts it to a HEX string
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static String hashMd5AsHex(String string) {
+ return getHexString(hashMd5(string.getBytes()));
+ }
+
+ /**
+ * Generates the MD5 Hash of a string. Use {@link StringHelper#getHexString(byte[])} to convert the byte array to a
+ * Hex String which is printable
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashMd5(String string) {
+ return hashMd5(string.getBytes());
+ }
+
+ /**
+ * Generates the MD5 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array to
+ * a Hex String which is printable
+ *
+ * @param bytes
+ * the bytes to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashMd5(byte[] bytes) {
+ return hash("MD5", bytes); //$NON-NLS-1$
+ }
+
+ /**
+ * Generates the SHA1 Hash of a string and converts it to a HEX String
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static String hashSha1AsHex(String string) {
+ return getHexString(hashSha1(string.getBytes()));
+ }
+
+ /**
+ * Generates the SHA1 Hash of a string Use {@link StringHelper#getHexString(byte[])} to convert the byte array to a
+ * Hex String which is printable
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashSha1(String string) {
+ return hashSha1(string.getBytes());
+ }
+
+ /**
+ * Generates the SHA1 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array
+ * to a Hex String which is printable
+ *
+ * @param bytes
+ * the bytes to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashSha1(byte[] bytes) {
+ return hash("SHA-1", bytes); //$NON-NLS-1$
+ }
+
+ /**
+ * Generates the SHA-256 Hash of a string and converts it to a HEX String
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static String hashSha256AsHex(String string) {
+ return getHexString(hashSha256(string.getBytes()));
+ }
+
+ /**
+ * Generates the SHA-256 Hash of a string Use {@link StringHelper#getHexString(byte[])} to convert the byte array to
+ * a Hex String which is printable
+ *
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashSha256(String string) {
+ return hashSha256(string.getBytes());
+ }
+
+ /**
+ * Generates the SHA1 Hash of a byte array Use {@link StringHelper#getHexString(byte[])} to convert the byte array
+ * to a Hex String which is printable
+ *
+ * @param bytes
+ * the bytes to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hashSha256(byte[] bytes) {
+ return hash("SHA-256", bytes); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the hash of an algorithm
+ *
+ * @param algorithm
+ * the algorithm to use
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static String hashAsHex(String algorithm, String string) {
+ return getHexString(hash(algorithm, string));
+ }
+
+ /**
+ * Returns the hash of an algorithm
+ *
+ * @param algorithm
+ * the algorithm to use
+ * @param string
+ * the string to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hash(String algorithm, String string) {
+ try {
+
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ byte[] hashArray = digest.digest(string.getBytes());
+
+ return hashArray;
+
+ } catch (NoSuchAlgorithmException e) {
+ String msg = MessageFormat.format("Algorithm {0} does not exist!", algorithm); //$NON-NLS-1$
+ throw new RuntimeException(msg, e);
+ }
+ }
+
+ /**
+ * Returns the hash of an algorithm
+ *
+ * @param algorithm
+ * the algorithm to use
+ * @param bytes
+ * the bytes to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static String hashAsHex(String algorithm, byte[] bytes) {
+ return getHexString(hash(algorithm, bytes));
+ }
+
+ /**
+ * Returns the hash of an algorithm
+ *
+ * @param algorithm
+ * the algorithm to use
+ * @param bytes
+ * the bytes to hash
+ *
+ * @return the hash or null, if an exception was thrown
+ */
+ public static byte[] hash(String algorithm, byte[] bytes) {
+ try {
+
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ byte[] hashArray = digest.digest(bytes);
+
+ return hashArray;
+
+ } catch (NoSuchAlgorithmException e) {
+ String msg = MessageFormat.format("Algorithm {0} does not exist!", algorithm); //$NON-NLS-1$
+ throw new RuntimeException(msg, e);
+ }
+ }
+
+ /**
+ * Normalizes the length of a String. Does not shorten it when it is too long, but lengthens it, depending on the
+ * options set: adding the char at the beginning or appending it at the end
+ *
+ * @param value
+ * string to normalize
+ * @param length
+ * length string must have
+ * @param beginning
+ * add at beginning of value
+ * @param c
+ * char to append when appending
+ * @return the new string
+ */
+ public static String normalizeLength(String value, int length, boolean beginning, char c) {
+ return normalizeLength(value, length, beginning, false, c);
+ }
+
+ /**
+ * Normalizes the length of a String. Shortens it when it is too long, giving out a logger warning, or lengthens it,
+ * depending on the options set: appending the char at the beginning or the end
+ *
+ * @param value
+ * string to normalize
+ * @param length
+ * length string must have
+ * @param beginning
+ * append at beginning of value
+ * @param shorten
+ * allow shortening of value
+ * @param c
+ * char to append when appending
+ * @return the new string
+ */
+ public static String normalizeLength(String value, int length, boolean beginning, boolean shorten, char c) {
+
+ if (value.length() == length)
+ return value;
+
+ if (value.length() < length) {
+
+ String tmp = value;
+ while (tmp.length() != length) {
+ if (beginning) {
+ tmp = c + tmp;
+ } else {
+ tmp = tmp + c;
+ }
+ }
+
+ return tmp;
+
+ } else if (shorten) {
+
+ logger.warn(MessageFormat.format("Shortening length of value: {0}", value)); //$NON-NLS-1$
+ logger.warn(MessageFormat.format("Length is: {0} max: {1}", value.length(), length)); //$NON-NLS-1$
+
+ return value.substring(0, length);
+ }
+
+ return value;
+ }
+
+ /**
+ * Calls {@link #replacePropertiesIn(Properties, String)}, with {@link System#getProperties()} as input
+ *
+ * @return a new string with all defined system properties replaced or if an error occurred the original value is
+ * returned
+ */
+ public static String replaceSystemPropertiesIn(String value) {
+ return replacePropertiesIn(System.getProperties(), value);
+ }
+
+ /**
+ * Traverses the given string searching for occurrences of ${...} sequences. Theses sequences are replaced with a
+ * {@link Properties#getProperty(String)} value if such a value exists in the properties map. If the value of the
+ * sequence is not in the properties, then the sequence is not replaced
+ *
+ * @param properties
+ * the {@link Properties} in which to get the value
+ * @param value
+ * the value in which to replace any system properties
+ *
+ * @return a new string with all defined properties replaced or if an error occurred the original value is returned
+ */
+ public static String replacePropertiesIn(Properties properties, String value) {
+
+ return replacePropertiesIn(properties, '$', value);
+ }
+
+ /**
+ * Traverses the given string searching for occurrences of prefix{...} sequences. Theses sequences are
+ * replaced with a {@link Properties#getProperty(String)} value if such a value exists in the properties map. If the
+ * value of the sequence is not in the properties, then the sequence is not replaced
+ *
+ * @param properties
+ * the {@link Properties} in which to get the value
+ * @param prefix
+ * the prefix to use, for instance use $ to replace occurrences of ${...}
+ * @param value
+ * the value in which to replace any system properties
+ *
+ * @return a new string with all defined properties replaced or if an error occurred the original value is returned
+ */
+ public static String replacePropertiesIn(Properties properties, char prefix, String value) {
+
+ String prefixS = String.valueOf(prefix);
+
+ // get a copy of the value
+ String tmpValue = value;
+
+ // get first occurrence of $ character
+ int pos = -1;
+ int stop = 0;
+
+ // loop on $ character positions
+ while ((pos = tmpValue.indexOf(prefix, pos + 1)) != -1) {
+
+ // if pos+1 is not { character then continue
+ if (tmpValue.charAt(pos + 1) != '{') {
+ continue;
+ }
+
+ // find end of sequence with } character
+ stop = tmpValue.indexOf('}', pos + 1);
+
+ // if no stop found, then break as another sequence should be able to start
+ if (stop == -1) {
+ logger.error(MessageFormat.format("Sequence starts at offset {0} but does not end!", pos)); //$NON-NLS-1$
+ tmpValue = value;
+ break;
+ }
+
+ // get sequence enclosed by pos and stop
+ String sequence = tmpValue.substring(pos + 2, stop);
+
+ // make sure sequence doesn't contain $ { } characters
+ if (sequence.contains(prefixS) || sequence.contains("{") || sequence.contains("}")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ String msg = "Enclosed sequence in offsets {0} - {1} contains one of the illegal chars: {2} { }: {3}";
+ msg = MessageFormat.format(msg, pos, stop, prefixS, sequence);
+ logger.error(msg);
+ tmpValue = value;
+ break;
+ }
+
+ // sequence is good, so see if we have a property for it
+ String property = properties.getProperty(sequence, StringHelper.EMPTY);
+
+ // if no property exists, then log and continue
+ if (property.isEmpty()) {
+ // logger.warn("No system property found for sequence " + sequence);
+ continue;
+ }
+
+ // property exists, so replace in value
+ tmpValue = tmpValue.replace(prefixS + "{" + sequence + "}", property); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ return tmpValue;
+ }
+
+ /**
+ * Calls {@link #replaceProperties(Properties, Properties)} with null as the second argument. This allows for
+ * replacing all properties with itself
+ *
+ * @param properties
+ * the properties in which the values must have any ${...} replaced by values of the respective key
+ */
+ public static void replaceProperties(Properties properties) {
+ replaceProperties(properties, null);
+ }
+
+ /**
+ * Checks every value in the {@link Properties} and then then replaces any ${...} variables with keys in this
+ * {@link Properties} value using {@link StringHelper#replacePropertiesIn(Properties, String)}
+ *
+ * @param properties
+ * the properties in which the values must have any ${...} replaced by values of the respective key
+ * @param altProperties
+ * if properties does not contain the ${...} key, then try these alternative properties
+ */
+ public static void replaceProperties(Properties properties, Properties altProperties) {
+
+ for (Object keyObj : properties.keySet()) {
+ String key = (String) keyObj;
+ String property = properties.getProperty(key);
+ String newProperty = replacePropertiesIn(properties, property);
+
+ // try first properties
+ if (!property.equals(newProperty)) {
+ // logger.info("Key " + key + " has replaced property " + property + " with new value " + newProperty);
+ properties.put(key, newProperty);
+ } else if (altProperties != null) {
+
+ // try alternative properties
+ newProperty = replacePropertiesIn(altProperties, property);
+ if (!property.equals(newProperty)) {
+ // logger.info("Key " + key + " has replaced property " + property + " from alternative properties with new value " + newProperty);
+ properties.put(key, newProperty);
+ }
+ }
+ }
+ }
+
+ /**
+ * This is a helper method with which it is possible to print the location in the two given strings where they start
+ * to differ. The length of string returned is currently 40 characters, or less if either of the given strings are
+ * shorter. The format of the string is 3 lines. The first line has information about where in the strings the
+ * difference occurs, and the second and third lines contain contexts
+ *
+ * @param s1
+ * the first string
+ * @param s2
+ * the second string
+ *
+ * @return the string from which the strings differ with a length of 40 characters within the original strings
+ */
+ public static String printUnequalContext(String s1, String s2) {
+
+ byte[] bytes1 = s1.getBytes();
+ byte[] bytes2 = s2.getBytes();
+ int i = 0;
+ for (; i < bytes1.length; i++) {
+ if (i > bytes2.length)
+ break;
+
+ if (bytes1[i] != bytes2[i])
+ break;
+ }
+
+ int maxContext = 40;
+ int start = Math.max(0, (i - maxContext));
+ int end = Math.min(i + maxContext, (Math.min(bytes1.length, bytes2.length)));
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Strings are not equal! Start of inequality is at " + i); //$NON-NLS-1$
+ sb.append(". Showing " + maxContext); //$NON-NLS-1$
+ sb.append(" extra characters and start and end:\n"); //$NON-NLS-1$
+ sb.append("context s1: "); //$NON-NLS-1$
+ sb.append(s1.substring(start, end));
+ sb.append("\n"); //$NON-NLS-1$
+ sb.append("context s2: "); //$NON-NLS-1$
+ sb.append(s2.substring(start, end));
+ sb.append("\n"); //$NON-NLS-1$
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the given number of milliseconds to a time like #h/m/s/ms/us/ns
+ *
+ * @param millis
+ * the number of milliseconds
+ *
+ * @return format the given number of milliseconds to a time like #h/m/s/ms/us/ns
+ */
+ public static String formatMillisecondsDuration(final long millis) {
+ return formatNanoDuration(millis * 1000000L);
+ }
+
+ /**
+ * Formats the given number of nanoseconds to a time like #h/m/s/ms/us/ns
+ *
+ * @param nanos
+ * the number of nanoseconds
+ *
+ * @return format the given number of nanoseconds to a time like #h/m/s/ms/us/ns
+ */
+ public static String formatNanoDuration(final long nanos) {
+ if (nanos >= 3600000000000L) {
+ return String.format("%.0fh", (nanos / 3600000000000.0D)); //$NON-NLS-1$
+ } else if (nanos >= 60000000000L) {
+ return String.format("%.0fm", (nanos / 60000000000.0D)); //$NON-NLS-1$
+ } else if (nanos >= 1000000000L) {
+ return String.format("%.0fs", (nanos / 1000000000.0D)); //$NON-NLS-1$
+ } else if (nanos >= 1000000L) {
+ return String.format("%.0fms", (nanos / 1000000.0D)); //$NON-NLS-1$
+ } else if (nanos >= 1000L) {
+ return String.format("%.0fus", (nanos / 1000.0D)); //$NON-NLS-1$
+ } else {
+ return nanos + "ns"; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * @see ExceptionHelper#formatException(Throwable)
+ */
+ public static String getExceptionMessage(Throwable t) {
+ return ExceptionHelper.getExceptionMessage(t);
+ }
+
+ /**
+ * @see ExceptionHelper#formatException(Throwable)
+ */
+ public static String formatException(Throwable t) {
+ return ExceptionHelper.formatException(t);
+ }
+
+ /**
+ * @see ExceptionHelper#formatExceptionMessage(Throwable)
+ */
+ public static String formatExceptionMessage(Throwable t) {
+ return ExceptionHelper.formatExceptionMessage(t);
+ }
+
+ /**
+ * Simply returns true if the value is null, or empty
+ *
+ * @param value
+ * the value to check
+ *
+ * @return true if the value is null, or empty
+ */
+ public static boolean isEmpty(String value) {
+ return value == null || value.isEmpty();
+ }
+
+ /**
+ * Simply returns true if the value is neither null nor empty
+ *
+ * @param value
+ * the value to check
+ *
+ * @return true if the value is neither null nor empty
+ */
+ public static boolean isNotEmpty(String value) {
+ return value != null && !value.isEmpty();
+ }
+
+ /**
+ *
+ * Parses the given string value to a boolean. This extends the default {@link Boolean#parseBoolean(String)} as it
+ * throws an exception if the string value is not equal to "true" or "false" being case insensitive.
+ *
+ *
+ *
+ * This additional restriction is important where false should really be caught, not any random vaue for false
+ *
+ *
+ * @param value
+ * the value to check
+ *
+ * @return true or false, depending on the string value
+ *
+ * @throws RuntimeException
+ * if the value is empty, or not equal to the case insensitive value "true" or "false"
+ */
+ public static boolean parseBoolean(String value) throws RuntimeException {
+ if (isEmpty(value))
+ throw new RuntimeException("Value to parse to boolean is empty! Expected case insensitive true or false"); //$NON-NLS-1$
+ String tmp = value.toLowerCase();
+ if (tmp.equals(Boolean.TRUE.toString())) {
+ return true;
+ } else if (tmp.equals(Boolean.FALSE.toString())) {
+ return false;
+ } else {
+ String msg = "Value {0} can not be parsed to boolean! Expected case insensitive true or false"; //$NON-NLS-1$
+ msg = MessageFormat.format(msg, value);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ public static String commaSeparated(String... values) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ sb.append(values[i]);
+ if (i < values.length - 1)
+ sb.append(", "); //$NON-NLS-1$
+ }
+ return sb.toString();
+ }
+
+ public static String[] splitCommaSeparated(String values) {
+ String[] split = values.split(","); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ split[i] = split[i].trim();
+ }
+ return split;
+ }
+
+ /**
+ * If the value parameter is empty, then a {@link #DASH} is returned, otherwise the value is returned
+ *
+ * @param value
+ *
+ * @return the non-empty value, or a {@link #DASH}
+ */
+ public static String valueOrDash(String value) {
+ if (isNotEmpty(value))
+ return value;
+ return DASH;
+ }
+
+ /**
+ * Return a pseudo unique id which is incremented on each call. The id is initialized from the current time
+ *
+ * @return a pseudo unique id
+ */
+ public static synchronized String getUniqueId() {
+ return Long.toString(getUniqueIdLong());
+ }
+
+ /**
+ * Return a pseudo unique id which is incremented on each call. The id is initialized from the current time
+ *
+ * @return a pseudo unique id
+ */
+ public static synchronized long getUniqueIdLong() {
+
+ if (uniqueId == Long.MAX_VALUE - 1) {
+ uniqueId = 0;
+ }
+
+ uniqueId += 1;
+ return uniqueId;
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java
new file mode 100644
index 000000000..f7f97d8f0
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2013 Robert von Burg
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ch.eitchnet.utils.helper;
+
+/**
+ * A helper class for {@link System} methods
+ *
+ * @author Robert von Burg
+ */
+public class SystemHelper {
+
+ private static final SystemHelper instance;
+
+ static {
+ instance = new SystemHelper();
+ }
+
+ public static SystemHelper getInstance() {
+ return SystemHelper.instance;
+ }
+
+ public static final String osName = System.getProperty("os.name"); //$NON-NLS-1$
+ public static final String osArch = System.getProperty("os.arch"); //$NON-NLS-1$
+ public static final String osVersion = System.getProperty("os.version"); //$NON-NLS-1$
+ public static final String javaVendor = System.getProperty("java.vendor"); //$NON-NLS-1$
+ public static final String javaVersion = System.getProperty("java.version"); //$NON-NLS-1$
+
+ /**
+ * private constructor
+ */
+ private SystemHelper() {
+ //
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return SystemHelper.asString();
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ public static String asString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(SystemHelper.osName);
+ sb.append(StringHelper.SPACE);
+ sb.append(SystemHelper.osArch);
+ sb.append(StringHelper.SPACE);
+ sb.append(SystemHelper.osVersion);
+ sb.append(StringHelper.SPACE);
+ sb.append("on Java "); //$NON-NLS-1$
+ sb.append(SystemHelper.javaVendor);
+ sb.append(" version "); //$NON-NLS-1$
+ sb.append(SystemHelper.javaVersion);
+ return sb.toString();
+ }
+
+ public static String getUserDir() {
+ return System.getProperty("user.dir"); //$NON-NLS-1$
+ }
+
+ public static boolean isMacOS() {
+ return SystemHelper.osName.startsWith("Mac"); //$NON-NLS-1$
+ }
+
+ public static boolean isWindows() {
+ return SystemHelper.osName.startsWith("Win"); //$NON-NLS-1$
+ }
+
+ public static boolean isLinux() {
+ return SystemHelper.osName.startsWith("Lin"); //$NON-NLS-1$
+ }
+
+ public static boolean is32bit() {
+ return SystemHelper.osArch.equals("x86") || SystemHelper.osArch.equals("i386") //$NON-NLS-1$ //$NON-NLS-2$
+ || SystemHelper.osArch.equals("i686"); //$NON-NLS-1$
+ }
+
+ public static boolean is64bit() {
+ return SystemHelper.osArch.equals("x86_64") || SystemHelper.osArch.equals("amd64"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ public static String getMaxMemory() {
+ return FileHelper.humanizeFileSize(Runtime.getRuntime().maxMemory());
+ }
+
+ public static String getUsedMemory() {
+ return FileHelper.humanizeFileSize(Runtime.getRuntime().totalMemory());
+ }
+
+ public static String getFreeMemory() {
+ return FileHelper.humanizeFileSize(Runtime.getRuntime().freeMemory());
+ }
+
+ public static String getMemorySummary() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Memory available "); //$NON-NLS-1$
+ sb.append(SystemHelper.getMaxMemory());
+ sb.append(" / Used: "); //$NON-NLS-1$
+ sb.append(SystemHelper.getUsedMemory());
+ sb.append(" / Free:"); //$NON-NLS-1$
+ sb.append(SystemHelper.getFreeMemory());
+ return sb.toString();
+ }
+}
diff --git a/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java
new file mode 100644
index 000000000..175df4922
--- /dev/null
+++ b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java
@@ -0,0 +1,264 @@
+package ch.eitchnet.utils.helper;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.KeyStore.TrustedCertificateEntry;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import javax.xml.crypto.dsig.CanonicalizationMethod;
+import javax.xml.crypto.dsig.DigestMethod;
+import javax.xml.crypto.dsig.Reference;
+import javax.xml.crypto.dsig.SignatureMethod;
+import javax.xml.crypto.dsig.SignedInfo;
+import javax.xml.crypto.dsig.Transform;
+import javax.xml.crypto.dsig.XMLSignature;
+import javax.xml.crypto.dsig.XMLSignatureFactory;
+import javax.xml.crypto.dsig.dom.DOMSignContext;
+import javax.xml.crypto.dsig.dom.DOMValidateContext;
+import javax.xml.crypto.dsig.keyinfo.KeyInfo;
+import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
+import javax.xml.crypto.dsig.keyinfo.X509Data;
+import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
+import javax.xml.crypto.dsig.spec.TransformParameterSpec;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import ch.eitchnet.utils.dbc.DBC;
+
+public class XmlDomSigner {
+
+ private static final Logger logger = LoggerFactory.getLogger(XmlDomSigner.class);
+
+ private KeyStore keyStore;
+ private String privateKeyAlias;
+ private String trustAlias;
+
+ private char[] password;
+
+ public XmlDomSigner(File keyStorePath, String privateKeyAlias, String trustAlias, char[] password) {
+
+ DBC.PRE.assertNotEmpty("privateKeyAlias", privateKeyAlias);
+ DBC.PRE.assertNotEmpty("trustAlias", trustAlias);
+ try {
+
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(new FileInputStream(keyStorePath), password);
+ this.keyStore = keyStore;
+ this.privateKeyAlias = privateKeyAlias;
+ this.trustAlias = trustAlias;
+ this.password = password;
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read keystore " + keyStorePath);
+ }
+ }
+
+ public void sign(Document document) throws RuntimeException {
+
+ try {
+
+ String id = "Signed_" + UUID.randomUUID().toString();
+ Element rootElement = document.getDocumentElement();
+ rootElement.setAttribute("ID", id);
+ rootElement.setIdAttribute("ID", true);
+
+ // Create a DOM XMLSignatureFactory that will be used to
+ // generate the enveloped signature.
+ XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
+
+ // Create a Reference to the enveloped document (in this case,
+ // you are signing the whole document, so a URI of "" signifies
+ // that, and also specify the SHA1 digest algorithm and
+ // the ENVELOPED Transform.
+ List transforms = new ArrayList<>();
+ transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
+ transforms.add(fac.newTransform(CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null));
+ DigestMethod digestMethod = fac.newDigestMethod(DigestMethod.SHA1, null);
+ Reference ref = fac.newReference("#" + id, digestMethod, transforms, null, null);
+
+ // Create the SignedInfo.
+ SignedInfo signedInfo = fac.newSignedInfo(
+ fac.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec) null), //
+ fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null), //
+ Collections.singletonList(ref));
+
+ // Load the KeyStore and get the signing key and certificate.
+ PrivateKeyEntry keyEntry = (PrivateKeyEntry) this.keyStore.getEntry(this.privateKeyAlias,
+ new KeyStore.PasswordProtection(this.password));
+ PrivateKey privateKey = keyEntry.getPrivateKey();
+ X509Certificate cert = (X509Certificate) keyEntry.getCertificate();
+
+ // Create the KeyInfo containing the X509Data.
+ KeyInfoFactory kif = fac.getKeyInfoFactory();
+ List