From 7017a8cfa0992d7a40f9ab63d6e8e365bfb7ae1a Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 8 Jun 2012 09:46:26 -0700 Subject: [PATCH 001/180] Initial commit --- .gitignore | 6 ++++++ README.md | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0f182a034 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.class + +# Package Files # +*.jar +*.war +*.ear diff --git a/README.md b/README.md new file mode 100644 index 000000000..c7088aa88 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +ch.eitchnet.java.utils +====================== + +Generic Java XML persistence layer. Implemented to be light-weight and simple to use \ No newline at end of file From 82368adaffd238c63811ef06f57db7554d10817d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 8 Jun 2012 18:59:51 +0200 Subject: [PATCH 002/180] [New] Initial commit of the utils This is initial commit includes * a RMI handler which can be used for easy up- and downloading of files over RMI * a FileHelper * a Log4jConfigurator * a StringHelper * a SystemHelper * a ObjectFilter which is used for caching operations to objects. These objects must implement ITransactionObject --- README.md | 3 +- lib/log4j-1.2.16-src.zip | Bin 0 -> 473361 bytes src/ch/eitchnet/rmi/RMIFileClient.java | 50 ++ src/ch/eitchnet/rmi/RmiFileDeletion.java | 40 ++ src/ch/eitchnet/rmi/RmiFileHandler.java | 249 +++++++ src/ch/eitchnet/rmi/RmiFilePart.java | 147 +++++ src/ch/eitchnet/rmi/RmiHelper.java | 221 +++++++ src/ch/eitchnet/utils/helper/FileHelper.java | 471 ++++++++++++++ .../utils/helper/Log4jConfigurator.java | 169 +++++ .../utils/helper/Log4jPropertyWatchDog.java | 104 +++ .../eitchnet/utils/helper/StringHelper.java | 397 ++++++++++++ .../eitchnet/utils/helper/SystemHelper.java | 73 +++ .../objectfilter/ITransactionObject.java | 37 ++ .../utils/objectfilter/ObjectCache.java | 112 ++++ .../utils/objectfilter/ObjectFilter.java | 611 ++++++++++++++++++ .../utils/objectfilter/Operation.java | 21 + 16 files changed, 2703 insertions(+), 2 deletions(-) create mode 100644 lib/log4j-1.2.16-src.zip create mode 100644 src/ch/eitchnet/rmi/RMIFileClient.java create mode 100644 src/ch/eitchnet/rmi/RmiFileDeletion.java create mode 100644 src/ch/eitchnet/rmi/RmiFileHandler.java create mode 100644 src/ch/eitchnet/rmi/RmiFilePart.java create mode 100644 src/ch/eitchnet/rmi/RmiHelper.java create mode 100644 src/ch/eitchnet/utils/helper/FileHelper.java create mode 100644 src/ch/eitchnet/utils/helper/Log4jConfigurator.java create mode 100644 src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java create mode 100644 src/ch/eitchnet/utils/helper/StringHelper.java create mode 100644 src/ch/eitchnet/utils/helper/SystemHelper.java create mode 100644 src/ch/eitchnet/utils/objectfilter/ITransactionObject.java create mode 100644 src/ch/eitchnet/utils/objectfilter/ObjectCache.java create mode 100644 src/ch/eitchnet/utils/objectfilter/ObjectFilter.java create mode 100644 src/ch/eitchnet/utils/objectfilter/Operation.java diff --git a/README.md b/README.md index c7088aa88..acb872905 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ ch.eitchnet.java.utils ====================== - -Generic Java XML persistence layer. Implemented to be light-weight and simple to use \ No newline at end of file +Java Utilites which ease daily work when programming in the Java language diff --git a/lib/log4j-1.2.16-src.zip b/lib/log4j-1.2.16-src.zip new file mode 100644 index 0000000000000000000000000000000000000000..086789a2076cfa7c88b567f012b5bbf32732c6ab GIT binary patch literal 473361 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 z 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

UKmIas$A(z~a^EA#X+ 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⊀`RWm9GuT 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%0Dcr!jwAda(Wo?xx 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 literal 0 HcmV?d00001 diff --git a/src/ch/eitchnet/rmi/RMIFileClient.java b/src/ch/eitchnet/rmi/RMIFileClient.java new file mode 100644 index 000000000..de93bb95a --- /dev/null +++ b/src/ch/eitchnet/rmi/RMIFileClient.java @@ -0,0 +1,50 @@ +package ch.eitchnet.rmi; + +import java.rmi.RemoteException; + +/** + * @author Robert von Burg + * + */ +public interface RMIFileClient { + + /** + * 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(RmiFilePart 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 RmiFileDeletion} 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(RmiFileDeletion fileDeletion) throws RemoteException; + + /** + * Remote method which a client can request part of a file. The server will fill the given {@link RmiFilePart} 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 RmiFilePart requestFile(RmiFilePart filePart) throws RemoteException; +} diff --git a/src/ch/eitchnet/rmi/RmiFileDeletion.java b/src/ch/eitchnet/rmi/RmiFileDeletion.java new file mode 100644 index 000000000..91e85c761 --- /dev/null +++ b/src/ch/eitchnet/rmi/RmiFileDeletion.java @@ -0,0 +1,40 @@ +package ch.eitchnet.rmi; + +import java.io.Serializable; + +/** + * @author Robert von Burg + */ +public class RmiFileDeletion 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 RmiFileDeletion(String fileName, String fileType) { + this.fileName = fileName; + this.fileType = fileType; + } + + /** + * @return the fileType + */ + public String getFileType() { + return fileType; + } + + /** + * @return the fileName + */ + public String getFileName() { + return fileName; + } +} diff --git a/src/ch/eitchnet/rmi/RmiFileHandler.java b/src/ch/eitchnet/rmi/RmiFileHandler.java new file mode 100644 index 000000000..c2a55a70c --- /dev/null +++ b/src/ch/eitchnet/rmi/RmiFileHandler.java @@ -0,0 +1,249 @@ +package ch.eitchnet.rmi; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.log4j.Logger; + +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(RmiFilePart)} and the downloading a file is done by calling {@link #requestFile(RmiFilePart)} + * + * @author Robert von Burg + */ +public class RmiFileHandler { + + private static final Logger logger = Logger.getLogger(RmiFileHandler.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; + + /** + * + */ + public RmiFileHandler(String basePath) { + + File basePathF = new File(basePath); + if (!basePathF.exists()) + throw new RuntimeException("Base Path does not exist " + basePathF.getAbsolutePath()); + if (!basePathF.canWrite()) + throw new RuntimeException("Can not write to base path " + basePathF.getAbsolutePath()); + + this.basePath = basePath; + } + + /** + * Method which a client can request part of a file. The server will fill the given {@link RmiFilePart} 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 RmiFilePart requestFile(RmiFilePart 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 + File file = new File(basePath + "/" + fileType, filePart.getFileName()); + + // now evaluate the file exists + if (!file.canRead()) { + throw new RuntimeException("The file " + fileName + + " could not be found in the location for files of type " + fileType); + } + + // 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 > MAX_PART_SIZE) { + throw new RuntimeException("The requested part size " + requestSize + " is greater than the allowed " + + MAX_PART_SIZE); + } + + // validate lengths and offsets + if (filePart.getFileLength() != fileSize) { + throw new RuntimeException("The part request has a file size " + filePart.getFileLength() + + ", but the file is actually " + fileSize); + } else if (requestOffset > fileSize) { + throw new RuntimeException("The requested file part offset " + requestOffset + + " is greater than the size of the file " + fileSize); + } + // 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) + throw new RuntimeException("Something went wrong. Min of requestSize and remaining is > MAX_PART_SIZE!"); + + // 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 + FileInputStream fin = null; + try { + + // position the stream + fin = new FileInputStream(file); + long skip = fin.skip(requestOffset); + if (skip != requestOffset) + throw new IOException("Asked to skip " + requestOffset + " but only skipped " + skip); + + // read the data + byte[] bytes = new byte[requestSize]; + int read = fin.read(bytes); + if (read != requestSize) + throw new IOException("Asked to read " + requestSize + " but only read " + read); + + // set the return result + filePart.setPartBytes(bytes); + + } catch (FileNotFoundException e) { + throw new RuntimeException("The file " + fileName + + " could not be found in the location for files of type " + fileType); + } catch (IOException e) { + throw new RuntimeException("There was an error while reading from the file " + fileName); + } finally { + if (fin != null) { + try { + fin.close(); + } catch (IOException e) { + logger.error("Error while closing FileInputStream: " + e.getLocalizedMessage()); + } + } + } + + // we are returning the same object as the user gave us, just edited + 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(RmiFilePart 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 + File dstFile = new File(basePath + "/" + fileType, filePart.getFileName()); + + // if the file already exists, then this may not be a start part + if (filePart.getPartOffset() == 0 && dstFile.exists()) { + throw new RuntimeException("The file " + fileName + " already exist for type " + fileType); + } + + // 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())) { + throw new RuntimeException("Uploading the file " + filePart.getFileName() + + " failed because the hashes don't match. Expected: " + filePart.getFileHash() + " / Actual: " + + dstFileHash); + } + } + } + + /** + * Method with which a client can delete files from the server. It only deletes single files if they exist + * + * @param fileDeletion + * the {@link RmiFileDeletion} defining the deletion request + * + * @return true if the file was deleted, false if the file did not exist + * + */ + public boolean deleteFile(RmiFileDeletion 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 + File fileToDelete = new File(basePath + "/" + fileType, fileDeletion.getFileName()); + + // delete the file + return FileHelper.deleteFiles(new File[] { fileToDelete }, true); + } + + /** + * 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!"); + } else if (fileName.contains("/")) { + throw new RuntimeException( + "The given file name contains illegal characters. The file name may not contain slashes!"); + } + } + + /** + * 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!"); + } else if (fileType.contains("/")) { + throw new RuntimeException( + "The given file type contains illegal characters. The file type may not contain slashes!"); + } + } +} diff --git a/src/ch/eitchnet/rmi/RmiFilePart.java b/src/ch/eitchnet/rmi/RmiFilePart.java new file mode 100644 index 000000000..a4a355a37 --- /dev/null +++ b/src/ch/eitchnet/rmi/RmiFilePart.java @@ -0,0 +1,147 @@ +package ch.eitchnet.rmi; + +import java.io.Serializable; + +/** + * @author Robert von Burg + */ +public class RmiFilePart 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 RmiFilePart(String fileName, String fileType) { + + if (fileName == null || fileName.isEmpty()) + throw new RuntimeException("fileName may not be empty!"); + if (fileType == null || fileType.isEmpty()) + throw new RuntimeException("fileType may not be empty!"); + + this.fileName = fileName; + this.fileType = fileType; + + this.partOffset = 0; + this.partLength = RmiFileHandler.MAX_PART_SIZE; + this.partBytes = null; + } + + /** + * @return the fileLength + */ + public long getFileLength() { + return fileLength; + } + + /** + * @param fileLength + * the fileLength to set + */ + public void setFileLength(long fileLength) { + this.fileLength = fileLength; + } + + /** + * @return the fileHash + */ + public String getFileHash() { + return fileHash; + } + + /** + * @param fileHash + * the fileHash to set + */ + public void setFileHash(String fileHash) { + this.fileHash = fileHash; + } + + /** + * @return the fileType + */ + public String getFileType() { + return fileType; + } + + /** + * @return the partOffset + */ + public long getPartOffset() { + return partOffset; + } + + /** + * @param partOffset + * the partOffset to set + */ + public void setPartOffset(long partOffset) { + this.partOffset = partOffset; + } + + /** + * @return the partLength + */ + public int getPartLength() { + return partLength; + } + + /** + * @param partLength + * the partLength to set + */ + public void setPartLength(int partLength) { + this.partLength = partLength; + } + + /** + * @return the partBytes + */ + public byte[] getPartBytes() { + return partBytes; + } + + /** + * @param partBytes + * the partBytes to set + */ + public void setPartBytes(byte[] partBytes) { + this.partBytes = partBytes; + } + + /** + * @return the lastPart + */ + public boolean isLastPart() { + return lastPart; + } + + /** + * @param lastPart + * the lastPart to set + */ + public void setLastPart(boolean lastPart) { + this.lastPart = lastPart; + } + + /** + * @return the fileName + */ + public String getFileName() { + return fileName; + } +} diff --git a/src/ch/eitchnet/rmi/RmiHelper.java b/src/ch/eitchnet/rmi/RmiHelper.java new file mode 100644 index 000000000..4755dce00 --- /dev/null +++ b/src/ch/eitchnet/rmi/RmiHelper.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2010 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.rmi; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.rmi.RemoteException; + +import org.apache.log4j.Logger; + +import ch.eitchnet.utils.helper.FileHelper; +import ch.eitchnet.utils.helper.StringHelper; + +/** + * @author Robert von Burg + * + */ +public class RmiHelper { + + private static final Logger logger = Logger.getLogger(RmiHelper.class); + + /** + * @param rmiFileClient + * @param filePart + * @param dstFile + */ + public static void downloadFile(RMIFileClient rmiFileClient, RmiFilePart filePart, File dstFile) { + + // here we don't overwrite, the caller must make sure the destination file does not exist + if (dstFile.exists()) + throw new RuntimeException("The destination file " + dstFile.getAbsolutePath() + + " already exists. Delete it first, if you want to overwrite it!"); + + try { + int loops = 0; + int startLength = filePart.getPartLength(); + while (true) { + loops += 1; + + // get the next part + filePart = rmiFileClient.requestFile(filePart); + + // validate length of data + if (filePart.getPartLength() != filePart.getPartBytes().length) + throw new RuntimeException("Invalid FilePart. Part length is not as long as the bytes passed " + + filePart.getPartLength() + " / " + filePart.getPartBytes().length); + + // validate offset is size of file + if (filePart.getPartOffset() != dstFile.length()) { + throw new RuntimeException("The part offset $offset is not at the end of the file " + + filePart.getPartOffset() + " / " + dstFile.length()); + } + + // append the part + FileHelper.appendFilePart(dstFile, filePart.getPartBytes()); + + // update the offset + filePart.setPartOffset(filePart.getPartOffset() + filePart.getPartBytes().length); + + // break if the offset is past the length of the file + if (filePart.getPartOffset() >= filePart.getFileLength()) + break; + } + logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Requested " + loops + + " parts. StartSize: " + startLength + " EndSize: " + filePart.getPartLength()); + + // validate that the offset is at the end of the file + if (filePart.getPartOffset() != filePart.getFileLength()) { + throw new RuntimeException("Offset " + filePart.getPartOffset() + " is not at file length " + + filePart.getFileLength() + " after reading all the file parts!"); + } + + // now validate hashes + String dstFileHash = StringHelper.getHexString(FileHelper.hashFileSha256(dstFile)); + if (!dstFileHash.equals(filePart.getFileHash())) { + throw new RuntimeException("Downloading the file " + filePart.getFileName() + + " failed because the hashes don't match. Expected: " + filePart.getFileHash() + " / Actual: " + + dstFileHash); + } + + } catch (Exception e) { + if (e instanceof RuntimeException) + throw (RuntimeException) e; + throw new RuntimeException("Downloading the file " + filePart.getFileName() + + " failed because of an underlying exception " + e.getLocalizedMessage()); + } + } + + /** + * @param rmiFileClient + * @param srcFile + * @param fileType + */ + public static void uploadFile(RMIFileClient rmiFileClient, File srcFile, String fileType) { + + // make sure the source file exists + if (!srcFile.canRead()) + throw new RuntimeException("The source file does not exist at " + srcFile.getAbsolutePath()); + + BufferedInputStream inputStream = null; + try { + + // get the size of the file + long fileLength = srcFile.length(); + String fileHash = StringHelper.getHexString(FileHelper.hashFileSha256(srcFile)); + + // create the file part to send + RmiFilePart filePart = new RmiFilePart(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 > RmiFileHandler.MAX_PART_SIZE) + partLength = RmiFileHandler.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 + inputStream = new BufferedInputStream(new FileInputStream(srcFile)); + + 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!"); + if (read != bytes.length) { + throw new IOException( + "Something went wrong while reading the bytes as the wrong number of bytes were read. Expected " + + bytes.length + " Actual: " + read); + } + + // 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 > RmiFileHandler.MAX_PART_SIZE) + throw new RuntimeException("Something went wrong as the remaining part " + remaining + + " is larger than MAX_PART_SIZE " + RmiFileHandler.MAX_PART_SIZE + "!"); + partLength = (int) remaining; + bytes = new byte[partLength]; + } + + // and save the offset for the next loop + offset = nextOffset; + } + + logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Sent " + loops + + " parts. StartSize: " + startLength + " EndSize: " + filePart.getPartLength()); + + } catch (Exception e) { + if (e instanceof RuntimeException) + throw (RuntimeException) e; + throw new RuntimeException("Uploading the file " + srcFile.getAbsolutePath() + + " failed because of an underlying exception " + e.getLocalizedMessage()); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + logger.error("Exception while closing FileInputStream " + e.getLocalizedMessage()); + } + } + } + } + + /** + * @param rmiFileClient + * @param fileDeletion + * @param dstFile + */ + public static void deleteFile(RMIFileClient rmiFileClient, RmiFileDeletion fileDeletion, File dstFile) { + + try { + rmiFileClient.deleteFile(fileDeletion); + } catch (RemoteException e) { + throw new RuntimeException("Deleting the file " + fileDeletion.getFileName() + + " failed because of an underlying exception " + e.getLocalizedMessage()); + } + } +} diff --git a/src/ch/eitchnet/utils/helper/FileHelper.java b/src/ch/eitchnet/utils/helper/FileHelper.java new file mode 100644 index 000000000..9e9de3735 --- /dev/null +++ b/src/ch/eitchnet/utils/helper/FileHelper.java @@ -0,0 +1,471 @@ +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.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.log4j.Logger; + +/** + * Helper class for dealing with files + * + * @author Robert von Burg + */ +public class FileHelper { + + private static final Logger logger = Logger.getLogger(FileHelper.class); + + /** + * 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"); + } + + return sb.toString(); + + } catch (FileNotFoundException e) { + throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException("Could not read file " + file.getAbsolutePath()); + } + } + + /** + * Writes the string to dstFile + * + * @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); + + bufferedwriter.close(); + + } catch (FileNotFoundException e) { + throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); + } + } + + /** + * 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 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 = deleteFiles(file.listFiles(), log); + if (!done) { + worked = false; + logger.warn("Could not empty the directory: " + file.getAbsolutePath()); + } else { + done = file.delete(); + if (done) { + if (log) + logger.info("Deleted DIR " + file.getAbsolutePath()); + } else { + worked = false; + logger.warn("Could not delete the directory: " + file.getAbsolutePath()); + } + } + } else { + boolean done = file.delete(); + if (done) { + if (log) + logger.info("Deleted FILE " + file.getAbsolutePath()); + } else { + worked = false; + logger.warn(("Could not delete the file: " + file.getAbsolutePath())); + } + } + } + return worked; + } + + /** + * 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) { + + BufferedInputStream inBuffer = null; + BufferedOutputStream outBuffer = null; + try { + + inBuffer = new BufferedInputStream(new FileInputStream(fromFile)); + outBuffer = new BufferedOutputStream(new FileOutputStream(toFile)); + + int theByte = 0; + + while ((theByte = inBuffer.read()) > -1) { + outBuffer.write(theByte); + } + + inBuffer.close(); + outBuffer.flush(); + outBuffer.close(); + + if (checksum) { + String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile)); + String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile)); + if (!fromFileMD5.equals(toFileMD5)) { + logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " + toFileMD5); + toFile.delete(); + + return false; + } + } + + // cleanup if files are not the same length + if (fromFile.length() != toFile.length()) { + logger.error("Copying failed, as new files are not the same length: " + fromFile.length() + " / " + + toFile.length()); + toFile.delete(); + + return false; + } + + } catch (Exception e) { + + logger.error(e, e); + return false; + + } finally { + + if (inBuffer != null) { + try { + inBuffer.close(); + } catch (IOException e) { + logger.error("Error closing BufferedInputStream" + e); + } + } + + if (outBuffer != null) { + try { + outBuffer.close(); + } catch (IOException e) { + logger.error("Error closing BufferedOutputStream" + e); + } + } + } + + 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; + } + + logger.warn("Simple File.renameTo failed, trying copy/delete..."); + + // delete if copy was successful, otherwise move will fail + if (FileHelper.copy(fromFile, toFile, true)) { + logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); + 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 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 + */ + 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 hashFile(file, "MD5"); + } + + /** + * 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 hashFile(file, "SHA-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 hashFile(file, "SHA-256"); + } + + /** + * 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); + fis.close(); + + return complete.digest(); + } catch (Exception e) { + throw new RuntimeException("Something went wrong while hashing file: " + file.getAbsolutePath()); + } + } + + /** + * 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) { + FileOutputStream outputStream = null; + try { + + 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()); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + logger.error("Exception while closing FileOutputStream " + e.getLocalizedMessage()); + } + } + } + } +} diff --git a/src/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/ch/eitchnet/utils/helper/Log4jConfigurator.java new file mode 100644 index 000000000..09150aa6b --- /dev/null +++ b/src/ch/eitchnet/utils/helper/Log4jConfigurator.java @@ -0,0 +1,169 @@ +package ch.eitchnet.utils.helper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.Properties; + +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.PropertyConfigurator; + +/** + * A simple configurator to configure log4j, with fall back default configuration + * + * @author Robert von Burg + */ +public class Log4jConfigurator { + + /** + * system property used to override the log4j configuration file + */ + public static final String PROP_FILE_LOG4J = "rsp.log4j.properties"; + + /** + * default log4j configuration file + */ + public static final String FILE_LOG4J = "log4j.properties"; + + /** + * runtime log4j configuration file which is a copy of the original file but has any place holders overwritten + */ + public static final String FILE_LOG4J_TEMP = "log4j.properties.tmp"; + + private static final Logger logger = Logger.getLogger(Log4jConfigurator.class); + private static Log4jPropertyWatchDog watchDog; + + /** + * Configures log4j with the default {@link ConsoleAppender} + */ + public static synchronized void configure() { + cleanupOldWatchdog(); + BasicConfigurator.resetConfiguration(); + BasicConfigurator.configure(new ConsoleAppender(getDefaulLayout())); + Logger.getRootLogger().setLevel(Level.INFO); + } + + /** + * Returns the default layout: %d %5p [%t] %C{1} %M - %m%n + * + * @return the default layout + */ + public static PatternLayout getDefaulLayout() { + return new PatternLayout("%d %5p [%t] %C{1} %M - %m%n"); + } + + /** + *

+ * Loads the log4j configuration + *

+ * + *

+ * This file is configurable through the {@link Log4jConfigurator#PROP_FILE_LOG4J} system property, but uses the + * default {@link Log4jConfigurator#FILE_LOG4J} file, if no configuration option is set. The path used is + * /config/ whereas is a system property + *

+ * + *

+ * Any properties in the properties are substituted using + * {@link StringHelper#replaceProperties(Properties, Properties)} and then the configuration file is written to a + * new file /tmp/{@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally + * {@link PropertyConfigurator#configureAndWatch(String)} is called so that the configuration is loaded and log4j + * watches the temporary file for configuration changes + *

+ */ + public static synchronized void loadLog4jConfiguration() { + + // first clean up any old watch dog in case of a programmatic re-load of hte configuration + cleanupOldWatchdog(); + + // get a configured log4j properties file, or use default RSPConfigConstants.FILE_LOG4J + String fileLog4j = SystemHelper.getProperty(Log4jConfigurator.class.getName(), + Log4jConfigurator.PROP_FILE_LOG4J, Log4jConfigurator.FILE_LOG4J); + + // get the root directory + String userDir = System.getProperty("user.dir"); + String configDir = userDir + "/config/"; + String tmpDir = userDir + "/tmp/"; + + // load the log4j.properties file + String pathNameToLog4j = configDir + fileLog4j; + File log4JPath = new File(pathNameToLog4j); + if (!log4JPath.exists()) + throw new RuntimeException("The log4j configuration file does not exist at " + log4JPath.getAbsolutePath()); + + String pathNameToLog4jTemp = tmpDir + Log4jConfigurator.FILE_LOG4J_TEMP; + Properties log4jProperties = new Properties(); + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(pathNameToLog4j); + log4jProperties.load(fin); + fin.close(); + + // replace any variables + StringHelper.replaceProperties(log4jProperties, System.getProperties()); + + // write this as the temporary log4j file + File logsFileDir = new File(tmpDir); + if (!logsFileDir.exists() && !logsFileDir.mkdirs()) + throw new RuntimeException("Could not create log path " + logsFileDir.getAbsolutePath()); + + fout = new FileOutputStream(pathNameToLog4jTemp); + log4jProperties.store(fout, "Running instance log4j configuration " + new Date()); + fout.close(); + + // XXX if the server is in a web context, then we may not use the FileWatchDog + BasicConfigurator.resetConfiguration(); + watchDog = new Log4jPropertyWatchDog(pathNameToLog4jTemp); + watchDog.start(); + + logger.info("Log4j is configured to use and watch file " + pathNameToLog4jTemp); + + } catch (Exception e) { + Log4jConfigurator.configure(); + logger.error(e, e); + logger.error("Log4j COULD NOT BE INITIALIZED. Please check the " + fileLog4j + " file at " + + pathNameToLog4j); + } finally { + if (fin != null) { + try { + fin.close(); + } catch (IOException e) { + logger.error("Exception closing input file: " + e, e); + } + } + if (fout != null) { + try { + fout.close(); + } catch (IOException e) { + logger.error("Exception closing output file: " + e, e); + } + } + } + } + + /** + * Cleanup a running watch dog + */ + public static synchronized void cleanupOldWatchdog() { + // clean up an old watch dog + if (watchDog != null) { + logger.info("Stopping old Log4j watchdog."); + watchDog.interrupt(); + try { + watchDog.join(1000l); + } catch (InterruptedException e) { + logger.error("Oops. Could not terminate an old WatchDog."); + } finally { + watchDog = null; + } + logger.info("Done."); + } + } +} diff --git a/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java new file mode 100644 index 000000000..fa177b74b --- /dev/null +++ b/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java @@ -0,0 +1,104 @@ +package ch.eitchnet.utils.helper; + +import java.io.File; + +import org.apache.log4j.LogManager; +import org.apache.log4j.PropertyConfigurator; +import org.apache.log4j.helpers.LogLog; + +/** + * @author Robert von Burg + * + */ +public class Log4jPropertyWatchDog extends Thread { + + /** + * The default delay between every file modification check, set to 60 seconds. + */ + public static final long DEFAULT_DELAY = 60000; + + /** + * The name of the file to observe for changes. + */ + protected String filename; + + /** + * The delay to observe between every check. By default set {@link #DEFAULT_DELAY}. + */ + protected long delay = DEFAULT_DELAY; + + protected File file; + protected long lastModif = 0; + protected boolean warnedAlready = false; + protected boolean interrupted = false; + + /** + * @param filename + */ + protected Log4jPropertyWatchDog(String filename) { + super("FileWatchdog"); + this.filename = filename; + file = new File(filename); + setDaemon(true); + checkAndConfigure(); + } + + /** + * Set the delay to observe between each check of the file changes. + */ + public void setDelay(long delay) { + this.delay = delay; + } + + /** + * + */ + protected void checkAndConfigure() { + boolean fileExists; + try { + fileExists = file.exists(); + } catch (SecurityException e) { + LogLog.warn("Was not allowed to read check file existance, file:[" + filename + "]."); + interrupted = true; // there is no point in continuing + return; + } + + if (fileExists) { + long l = file.lastModified(); // this can also throw a SecurityException + if (l > lastModif) { // however, if we reached this point this + lastModif = l; // is very unlikely. + doOnChange(); + warnedAlready = false; + } + } else { + if (!warnedAlready) { + LogLog.debug("[" + filename + "] does not exist."); + warnedAlready = true; + } + } + } + + /** + * Call {@link PropertyConfigurator#configure(String)} with the filename to reconfigure log4j. + */ + public void doOnChange() { + PropertyConfigurator propertyConfigurator = new PropertyConfigurator(); + propertyConfigurator.doConfigure(filename, LogManager.getLoggerRepository()); + } + + /** + * @see java.lang.Thread#run() + */ + @Override + public void run() { + while (!interrupted) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // no interruption expected + interrupted = true; + } + checkAndConfigure(); + } + } +} diff --git a/src/ch/eitchnet/utils/helper/StringHelper.java b/src/ch/eitchnet/utils/helper/StringHelper.java new file mode 100644 index 000000000..d8ed6d588 --- /dev/null +++ b/src/ch/eitchnet/utils/helper/StringHelper.java @@ -0,0 +1,397 @@ +package ch.eitchnet.utils.helper; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Properties; + +import org.apache.log4j.Logger; + +/** + * A helper class to perform different actions on {@link String}s + * + * @author Robert von Burg + */ +public class StringHelper { + + private static final Logger logger = Logger.getLogger(StringHelper.class); + + /** + * Hex char table for fast calculating of hex value + */ + 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"); + + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Something went wrong while converting to HEX: " + e.getLocalizedMessage(), 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."); + + 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 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); + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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) { + throw new RuntimeException("Algorithm " + algorithm + " does not exist!", 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) { + + while (value.length() != length) { + if (beginning) { + value = c + value; + } else { + value = value + c; + } + } + + return value; + + } else if (shorten) { + + logger.warn("Shortening length of value: " + value); + logger.warn("Length is: " + value.length() + " max: " + length); + + 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) { + + // keep copy of original value + String origValue = value; + + // get first occurrence of $ character + int pos = -1; + int stop = 0; + + // loop on $ character positions + while ((pos = value.indexOf('$', pos + 1)) != -1) { + + // if pos+1 is not { character then continue + if (value.charAt(pos + 1) != '{') { + continue; + } + + // find end of sequence with } character + stop = value.indexOf('}', pos + 1); + + // if no stop found, then break as another sequence should be able to start + if (stop == -1) { + logger.error("Sequence starts at offset " + pos + " but does not end!"); + value = origValue; + break; + } + + // get sequence enclosed by pos and stop + String sequence = value.substring(pos + 2, stop); + + // make sure sequence doesn't contain $ { } characters + if (sequence.contains("$") || sequence.contains("{") || sequence.contains("}")) { + logger.error("Enclosed sequence in offsets " + pos + " - " + stop + + " contains one of the illegal chars: $ { }: " + sequence); + value = origValue; + break; + } + + // sequence is good, so see if we have a property for it + String property = properties.getProperty(sequence, ""); + + // 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 + value = value.replace("${" + sequence + "}", property); + } + + return value; + } + + /** + * 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 = StringHelper.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 = StringHelper.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 + ". Showing " + maxContext + + " extra characters and start and end:\n"); + sb.append("context s1: " + s1.substring(start, end) + "\n"); + sb.append("context s2: " + s2.substring(start, end) + "\n"); + + return sb.toString(); + } +} diff --git a/src/ch/eitchnet/utils/helper/SystemHelper.java b/src/ch/eitchnet/utils/helper/SystemHelper.java new file mode 100644 index 000000000..070fe7272 --- /dev/null +++ b/src/ch/eitchnet/utils/helper/SystemHelper.java @@ -0,0 +1,73 @@ +package ch.eitchnet.utils.helper; + +/** + * A helper class for {@link System} methods + * + * @author Robert von Burg + */ +public class SystemHelper { + + /** + * 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 { + String property = System.getProperty(key, def); + if (property == null) + throw new RuntimeException("[" + context + "] Property " + key + " is not set, and no default was given!"); + + return property; + } + + /** + * 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(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(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())); + } +} diff --git a/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java b/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java new file mode 100644 index 000000000..a9a88b783 --- /dev/null +++ b/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java @@ -0,0 +1,37 @@ +package ch.eitchnet.utils.objectfilter; + +/** + * This interface serves for objects which are required, at some point, to have a unique ID within a transaction. + * + * @author Michael Gatto + */ +public interface ITransactionObject { + + /** + * UNSET Marker to determine if ids have not been set. + *

+ * Beware: this is set to 0 due to transient field in the {@link ITransactionObject} implementations that store the + * ID, which are set to zero when de-serialized, and that are not allowed to be serialized. + *

+ */ + public static final long UNSET = 0; + + /** + * Set the ID of this object. This ID is unique for this object within the transaction. + * + * @param id + * The ID to set. + */ + public void setTransactionID(long id); + + /** + * @return The ID of this object, as set within the transaction. This ID shall guarantee that it is unique within + * this transaction. + */ + public long getTransactionID(); + + /** + * Reset / anul the transaction ID of this object + */ + public void resetTransactionID(); +} diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/ch/eitchnet/utils/objectfilter/ObjectCache.java new file mode 100644 index 000000000..9e4aeb134 --- /dev/null +++ b/src/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -0,0 +1,112 @@ +package ch.eitchnet.utils.objectfilter; + +import org.apache.log4j.Logger; + +/** + * This class is a cache for objects whose operations (additions, modifications, removals) are first collected and then + * "deployed" in one go. + *

+ * Thus, this class keeps: + *

    + *
  • An ID of the object, such that it can be referenced externally. + *
  • A key for an object, which keeps the object's type.
  • + *
  • A reference to the current state of the object
  • + *
  • An identifier of the operation that needs to be performed on this
  • + *
+ *

+ * + * @author Michael Gatto + * + * @param + */ +public class ObjectCache { + + private final static Logger logger = Logger.getLogger(ObjectCache.class); + + /** + * id The unique ID of this object in this session + */ + private final long id; + /** + * key The key defining who's registered for this object's state + */ + private final String key; + /** + * object The object that shall be cached + */ + private T object; + /** + * operation The operation that has occurred on this object. + */ + private Operation operation; + + /** + * @param key + * @param object + * @param operation + */ + public ObjectCache(String key, T object, Operation operation) { + + this.id = object.getTransactionID(); + this.key = key; + this.object = object; + this.operation = operation; + + if (logger.isDebugEnabled()) { + logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + " / " + + object.toString()); + } + } + + /** + * Set the new object version of this cache. + * + * @param object + */ + public void setObject(T object) { + if (logger.isDebugEnabled()) { + logger.debug("Updating ID " + this.id + " to value " + object.toString()); + } + this.object = object; + } + + /** + * Change the operation to execute for this object. + * + * @param newOperation + */ + public void setOperation(Operation newOperation) { + if (logger.isDebugEnabled()) { + logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " + newOperation); + } + this.operation = newOperation; + } + + /** + * @return the id + */ + public long getId() { + return id; + } + + /** + * @return the key + */ + public String getKey() { + return key; + } + + /** + * @return the object + */ + public T getObject() { + return object; + } + + /** + * @return the operation + */ + public Operation getOperation() { + return operation; + } +} diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java new file mode 100644 index 000000000..76dded822 --- /dev/null +++ b/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -0,0 +1,611 @@ +package ch.eitchnet.utils.objectfilter; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.apache.log4j.Logger; + +/** + * This class implements a filter where modifications to an object are collected, and only the most recent action and + * version of the object is returned. + *

+ * In its current implementation, any instance of an object may "live" in the cache registered only under one single + * key. + *

+ *

+ * The rules of the cache are defined as follows. The top row represents the state of the cache for an object, given in + * version O1. Here, "N/A" symbolizes that the object is not yet present in the cache or, if it is a result of an + * operation, that it is removed from the cache. Each other row symbolizes the next action that is performed on an + * object, with the cell containing the "final" action for this object with the version of the object to be retained. + * Err! symbolize incorrect sequences of events that cause an exception. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Action \ State in CacheN/AAdd(01)Update(O1)Remove(O1)
Add (O2)Add(O2)Err!Err!Update(O2)
Update (O2)Update(O2)Add(O2)Update(O2)Err!
Remove (O2)Remove(O2)N/ARemove(O2)Err!
+ * + * @author Michael Gatto (initial version) + * @author Robert von Burg + * + * @param + */ +public class ObjectFilter { + + // XXX think about removing the generic T, as there is no sense in it + + private final static Logger logger = Logger.getLogger(ObjectFilter.class); + + private HashMap> cache = new HashMap>(); + private HashSet keySet = new HashSet(); + + private static long id = ITransactionObject.UNSET; + + /** + * Register, under the given key, the addition of the given object. + *

+ * This is the single point where the updating logic is applied for the cache in case of addition. The logic is: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Action\State in CacheN/AAdd(01)Update(O1)Remove(O1)
Add (O2)Add(O2)Err!Err!Update(O2)
+ * + * @param key + * the key to register the object with + * @param objectToAdd + * The object for which addition shall be registered. + */ + public void add(String key, T objectToAdd) { + + if (logger.isDebugEnabled()) + logger.debug("add object " + objectToAdd + " with key " + key); + + // add the key to the set + keySet.add(key); + + // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. + long id = objectToAdd.getTransactionID(); + if (id == ITransactionObject.UNSET) { + // The ID of the object has not been set, so it has not been in the cache during this + // run. Hence, we create an ID and add it to the cache. + id = dispenseID(); + objectToAdd.setTransactionID(id); + ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); + cache.put(id, cacheObj); + } else { + ObjectCache cached = cache.get(Long.valueOf(objectToAdd.getTransactionID())); + if (cached == null) { + // The object got an ID during this run, but was not added to the cache. + // Hence, we add it now, with the current operation. + ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); + cache.put(id, cacheObj); + } else { + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + throw new RuntimeException( + "Invalid key provided for object with transaction ID " + + Long.toString(id) + + " and operation " + + Operation.ADD.toString() + + ": existing key is " + + existingKey + + ", new key is " + + key + + ". Object may be present in the same filter instance only once, registered using one key only. Object:" + + objectToAdd.toString()); + } + // The object is in cache: update the version as required, keeping in mind that most + // of the cases here will be mistakes... + Operation op = cached.getOperation(); + switch (op) { + case ADD: + throw new RuntimeException("Stale State exception. Invalid + after +"); + case MODIFY: + throw new RuntimeException("Stale State exception. Invalid + after +="); + case REMOVE: + cached.setObject(objectToAdd); + cached.setOperation(Operation.MODIFY); + break; + } // switch + }// else of object not in cache + }// else of ID not set + } + + /** + * Register, under the given key, the update of the given object. *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Action \ State in CacheN/AAdd(01)Update(O1)Remove(O1)
Update (O2)Update(O2)Add(O2)Update(O2)Err!
+ * + * @param key + * the key to register the object with + * @param objectToUpdate + * The object for which update shall be registered. + */ + public void update(String key, T objectToUpdate) { + + if (logger.isDebugEnabled()) + logger.debug("update object " + objectToUpdate + " with key " + key); + + // add the key to the keyset + keySet.add(key); + // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. + + long id = objectToUpdate.getTransactionID(); + if (id == ITransactionObject.UNSET) { + id = dispenseID(); + objectToUpdate.setTransactionID(id); + ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); + cache.put(id, cacheObj); + } else { + ObjectCache cached = cache.get(Long.valueOf(objectToUpdate.getTransactionID())); + if (cached == null) { + // The object got an ID during this run, but was not added to this cache. + // Hence, we add it now, with the current operation. + ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); + cache.put(id, cacheObj); + } else { + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + + throw new RuntimeException( + "Invalid key provided for object with transaction ID " + + Long.toString(id) + + " and operation " + + Operation.MODIFY.toString() + + ": existing key is " + + existingKey + + ", new key is " + + key + + ". Object may be present in the same filter instance only once, registered using one key only. Object:" + + objectToUpdate.toString()); + } + // The object is in cache: update the version as required. + Operation op = cached.getOperation(); + switch (op) { + case ADD: + cached.setObject(objectToUpdate); + break; + case MODIFY: + cached.setObject(objectToUpdate); + break; + case REMOVE: + throw new RuntimeException("Stale State exception: Invalid += after -"); + } // switch + }// else of object not in cache + }// else of ID not set + } + + /** + * Register, under the given key, the removal of the given object. *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Action \ State in CacheN/AAdd(01)Update(O1)Remove(O1)
Remove (O2)Remove(O2)N/ARemove(O2)Err!
+ * + * @param key + * the key to register the object with + * @param objectToRemove + * The object for which removal shall be registered. + */ + public void remove(String key, T objectToRemove) { + + if (logger.isDebugEnabled()) + logger.debug("remove object " + objectToRemove + " with key " + key); + + // add the key to the keyset + keySet.add(key); + // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. + long id = objectToRemove.getTransactionID(); + if (id == ITransactionObject.UNSET) { + id = dispenseID(); + objectToRemove.setTransactionID(id); + ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); + cache.put(id, cacheObj); + } else { + ObjectCache cached = cache.get(Long.valueOf(id)); + if (cached == null) { + // The object got an ID during this run, but was not added to this cache. + // Hence, we add it now, with the current operation. + ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); + cache.put(id, cacheObj); + } else { + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + throw new RuntimeException( + "Invalid key provided for object with transaction ID " + + Long.toString(id) + + " and operation " + + Operation.REMOVE.toString() + + ": existing key is " + + existingKey + + ", new key is " + + key + + ". Object may be present in the same filter instance only once, registered using one key only. Object:" + + objectToRemove.toString()); + } + // The object is in cache: update the version as required. + Operation op = cached.getOperation(); + switch (op) { + case ADD: + // this is a case where we're removing the object from the cache, since we are + // removing it now and it was added previously. + cache.remove(Long.valueOf(id)); + break; + case MODIFY: + cached.setObject(objectToRemove); + cached.setOperation(Operation.REMOVE); + break; + case REMOVE: + throw new RuntimeException("Stale State exception. Invalid - after -"); + } // switch + } + } + } + + /** + * Register, under the given key, the addition of the given list of objects. + * + * @param key + * the key to register the objects with + * @param addedObjects + * The objects for which addition shall be registered. + */ + public void addAll(String key, Collection addedObjects) { + for (T addObj : addedObjects) { + add(key, addObj); + } + } + + /** + * Register, under the given key, the update of the given list of objects. + * + * @param key + * the key to register the objects with + * @param updatedObjects + * The objects for which update shall be registered. + */ + public void updateAll(String key, Collection updatedObjects) { + for (T update : updatedObjects) { + update(key, update); + } + } + + /** + * Register, under the given key, the removal of the given list of objects. + * + * @param key + * the key to register the objects with + * @param removedObjects + * The objects for which removal shall be registered. + */ + public void removeAll(String key, Collection removedObjects) { + for (T removed : removedObjects) { + remove(key, removed); + } + } + + /** + * Register the addition of the given object. Since no key is provided, the class name is used as a key. + * + * @param object + * The object that shall be registered for addition + */ + public void add(T object) { + add(object.getClass().getName(), object); + } + + /** + * Register the update of the given object. Since no key is provided, the class name is used as a key. + * + * @param object + * The object that shall be registered for updating + */ + public void update(T object) { + update(object.getClass().getName(), object); + } + + /** + * Register the removal of the given object. Since no key is provided, the class name is used as a key. + * + * @param object + * The object that shall be registered for removal + */ + public void remove(T object) { + remove(object.getClass().getName(), object); + } + + /** + * Register the addition of all objects in the list. Since no key is provided, the class name of each object is used + * as a key. + * + * @param objects + * The objects that shall be registered for addition + */ + public void addAll(List objects) { + for (T addedObj : objects) { + add(addedObj.getClass().getName(), addedObj); + } + } + + /** + * Register the updating of all objects in the list. Since no key is provided, the class name of each object is used + * as a key. + * + * @param updateObjects + * The objects that shall be registered for updating + */ + public void updateAll(List updateObjects) { + for (T update : updateObjects) { + update(update.getClass().getName(), update); + } + } + + /** + * Register the removal of all objects in the list. Since no key is provided, the class name of each object is used + * as a key. + * + * @param removedObjects + * The objects that shall be registered for removal + */ + public void removeAll(List removedObjects) { + for (T removed : removedObjects) { + remove(removed.getClass().getName(), removed); + } + } + + /** + * Get all objects that were registered under the given key and that have as a resulting final action an addition. + * + * @param key + * The registration key of the objects to match + * @return The list of all objects registered under the given key and that need to be added. + */ + public List getAdded(String key) { + List addedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { + addedObjects.add(objectCache.getObject()); + } + } + return addedObjects; + } + + /** + * Get all objects that were registered under the given key and that have as a resulting final action an addition. + * + * @param clazz + * The class type of the object to be retrieved, that acts as an additional filter criterion. + * @param key + * The registration key of the objects to match + * @return The list of all objects registered under the given key and that need to be added. + */ + public List getAdded(Class clazz, String key) { + List addedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { + if (objectCache.getObject().getClass() == clazz) { + @SuppressWarnings("unchecked") + V object = (V) objectCache.getObject(); + addedObjects.add(object); + } + } + } + return addedObjects; + } + + /** + * Get all objects that were registered under the given key and that have as a resulting final action an update. + * + * @param key + * registration key of the objects to match + * @return The list of all objects registered under the given key and that need to be updated. + */ + public List getUpdated(String key) { + List updatedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { + updatedObjects.add(objectCache.getObject()); + } + } + return updatedObjects; + } + + /** + * Get all objects that were registered under the given key and that have as a resulting final action an update. + * + * @param key + * registration key of the objects to match + * @return The list of all objects registered under the given key and that need to be updated. + */ + public List getUpdated(Class clazz, String key) { + List updatedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { + if (objectCache.getObject().getClass() == clazz) { + @SuppressWarnings("unchecked") + V object = (V) objectCache.getObject(); + updatedObjects.add(object); + } + } + } + return updatedObjects; + } + + /** + * Get all objects that were registered under the given key that have as a resulting final action their removal. + * + * @param key + * The registration key of the objects to match + * @return The list of object registered under the given key that have, as a final action, removal. + */ + public List getRemoved(String key) { + List removedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { + removedObjects.add(objectCache.getObject()); + } + } + return removedObjects; + } + + /** + * Get all objects that were registered under the given key that have as a resulting final action their removal. + * + * @param key + * The registration key of the objects to match + * @return The list of object registered under the given key that have, as a final action, removal. + */ + public List getRemoved(Class clazz, String key) { + List removedObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { + if (objectCache.getObject().getClass() == clazz) { + @SuppressWarnings("unchecked") + V object = (V) objectCache.getObject(); + removedObjects.add(object); + } + } + } + return removedObjects; + } + + /** + * Get all the objects that were processed in this transaction, that were registered under the given key. No action + * is associated to the object. + * + * @param key + * The registration key for which the objects shall be retrieved + * @return The list of objects matching the given key. + */ + public List getAll(String key) { + List allObjects = new LinkedList(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getKey().equals(key)) { + allObjects.add(objectCache.getObject()); + } + } + return allObjects; + } + + /** + * Get all the objects that were processed in this transaction, that were registered under the given key. No action + * is associated to the object. + * + * @param key + * The registration key for which the objects shall be retrieved + * @return The list of objects matching the given key. + */ + public List> getCache(String key) { + List> allCache = new LinkedList>(); + Collection> allObjs = cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getKey().equals(key)) { + allCache.add(objectCache); + } + } + return allCache; + } + + /** + * Return the set of keys that are currently present in the object filter. + * + * @return The set containing the keys of that have been added to the filter. + */ + public Set keySet() { + return keySet; + } + + /** + * Clear the cache. + */ + public void clearCache() { + cache.clear(); + keySet.clear(); + } + + /** + * @return get a unique transaction ID + */ + public synchronized long dispenseID() { + id++; + if (id == Long.MAX_VALUE) { + logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); + id = 1; + } + return id; + } +} diff --git a/src/ch/eitchnet/utils/objectfilter/Operation.java b/src/ch/eitchnet/utils/objectfilter/Operation.java new file mode 100644 index 000000000..1ca926c80 --- /dev/null +++ b/src/ch/eitchnet/utils/objectfilter/Operation.java @@ -0,0 +1,21 @@ +package ch.eitchnet.utils.objectfilter; + +/** + * A discrete set of operations associated to some object / state + * + * @author Michael Gatto + */ +public enum Operation { + /** + * ADD The operation associated to something is addition. + */ + ADD, + /** + * MODIFY The operation associated to something is a modification. + */ + MODIFY, + /** + * REMOVE The operation associated to something is removal + */ + REMOVE; +} From 97bd5cf5a4ca3f9ee1c7adb7fee4c3543c265db7 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 14 Jun 2012 23:38:50 +0200 Subject: [PATCH 003/180] [New] Added packaging information and files and licence This includes setting the licence to LGPL, setting the headers, adding a build script and testing the build --- COPYING | 674 ++++++++++++++++++ COPYING.LESSER | 165 +++++ build.xml | 73 ++ src/ch/eitchnet/rmi/RMIFileClient.java | 24 + src/ch/eitchnet/rmi/RmiFileDeletion.java | 24 + src/ch/eitchnet/rmi/RmiFileHandler.java | 24 + src/ch/eitchnet/rmi/RmiFilePart.java | 24 + src/ch/eitchnet/rmi/RmiHelper.java | 24 +- src/ch/eitchnet/utils/helper/FileHelper.java | 24 + .../utils/helper/Log4jConfigurator.java | 24 + .../utils/helper/Log4jPropertyWatchDog.java | 24 + .../eitchnet/utils/helper/StringHelper.java | 24 + .../eitchnet/utils/helper/SystemHelper.java | 24 + .../objectfilter/ITransactionObject.java | 24 + .../utils/objectfilter/ObjectCache.java | 24 + .../utils/objectfilter/ObjectFilter.java | 24 + .../utils/objectfilter/Operation.java | 24 + 17 files changed, 1243 insertions(+), 5 deletions(-) create mode 100644 COPYING create mode 100644 COPYING.LESSER create mode 100644 build.xml diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/COPYING.LESSER b/COPYING.LESSER new file mode 100644 index 000000000..65c5ca88a --- /dev/null +++ b/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..ee31a36fb --- /dev/null +++ b/build.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Cleaning build folder ${buildFolder} + + + + + + + Cleaning dist folder ${distFolder} + + + + + + + diff --git a/src/ch/eitchnet/rmi/RMIFileClient.java b/src/ch/eitchnet/rmi/RMIFileClient.java index de93bb95a..d5ad7c2cd 100644 --- a/src/ch/eitchnet/rmi/RMIFileClient.java +++ b/src/ch/eitchnet/rmi/RMIFileClient.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.rmi; import java.rmi.RemoteException; diff --git a/src/ch/eitchnet/rmi/RmiFileDeletion.java b/src/ch/eitchnet/rmi/RmiFileDeletion.java index 91e85c761..0c68caba3 100644 --- a/src/ch/eitchnet/rmi/RmiFileDeletion.java +++ b/src/ch/eitchnet/rmi/RmiFileDeletion.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.rmi; import java.io.Serializable; diff --git a/src/ch/eitchnet/rmi/RmiFileHandler.java b/src/ch/eitchnet/rmi/RmiFileHandler.java index c2a55a70c..47aba008b 100644 --- a/src/ch/eitchnet/rmi/RmiFileHandler.java +++ b/src/ch/eitchnet/rmi/RmiFileHandler.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.rmi; import java.io.File; diff --git a/src/ch/eitchnet/rmi/RmiFilePart.java b/src/ch/eitchnet/rmi/RmiFilePart.java index a4a355a37..8bddfed1c 100644 --- a/src/ch/eitchnet/rmi/RmiFilePart.java +++ b/src/ch/eitchnet/rmi/RmiFilePart.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.rmi; import java.io.Serializable; diff --git a/src/ch/eitchnet/rmi/RmiHelper.java b/src/ch/eitchnet/rmi/RmiHelper.java index 4755dce00..96aa27879 100644 --- a/src/ch/eitchnet/rmi/RmiHelper.java +++ b/src/ch/eitchnet/rmi/RmiHelper.java @@ -1,11 +1,25 @@ /* - * Copyright (c) 2010 - 2011 + * Copyright (c) 2012 * - * Apixxo AG - * Hauptgasse 25 - * 4600 Olten + * Robert von Burg * - * All rights reserved. + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/ch/eitchnet/utils/helper/FileHelper.java b/src/ch/eitchnet/utils/helper/FileHelper.java index 9e9de3735..4f310ec8a 100644 --- a/src/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/ch/eitchnet/utils/helper/FileHelper.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.helper; import java.io.BufferedInputStream; diff --git a/src/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/ch/eitchnet/utils/helper/Log4jConfigurator.java index 09150aa6b..e70672640 100644 --- a/src/ch/eitchnet/utils/helper/Log4jConfigurator.java +++ b/src/ch/eitchnet/utils/helper/Log4jConfigurator.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.helper; import java.io.File; diff --git a/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java index fa177b74b..a41f8dc2a 100644 --- a/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java +++ b/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.helper; import java.io.File; diff --git a/src/ch/eitchnet/utils/helper/StringHelper.java b/src/ch/eitchnet/utils/helper/StringHelper.java index d8ed6d588..c5807b2c1 100644 --- a/src/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/ch/eitchnet/utils/helper/StringHelper.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.helper; import java.io.UnsupportedEncodingException; diff --git a/src/ch/eitchnet/utils/helper/SystemHelper.java b/src/ch/eitchnet/utils/helper/SystemHelper.java index 070fe7272..2449d9038 100644 --- a/src/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/ch/eitchnet/utils/helper/SystemHelper.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Robert von Burg + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.helper; /** diff --git a/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java b/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java index a9a88b783..a243ed9ba 100644 --- a/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java +++ b/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Michael Gatto + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.objectfilter; /** diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/ch/eitchnet/utils/objectfilter/ObjectCache.java index 9e4aeb134..b329c4f0b 100644 --- a/src/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Michael Gatto + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.objectfilter; import org.apache.log4j.Logger; diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java index 76dded822..4a29eec99 100644 --- a/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Michael Gatto + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.objectfilter; import java.util.Collection; diff --git a/src/ch/eitchnet/utils/objectfilter/Operation.java b/src/ch/eitchnet/utils/objectfilter/Operation.java index 1ca926c80..8d23723ba 100644 --- a/src/ch/eitchnet/utils/objectfilter/Operation.java +++ b/src/ch/eitchnet/utils/objectfilter/Operation.java @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 + * + * Michael Gatto + * + */ + +/* + * This file is part of ch.eitchnet.java.utils + * + * Privilege is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Privilege. If not, see . + * + */ package ch.eitchnet.utils.objectfilter; /** From 2a3222ba66daa2575c24f9f24bd5cb9afc340efe Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 28 Jul 2012 15:34:37 +0200 Subject: [PATCH 004/180] [Major] rebuilt project using maven - Now "mvn compile" will build the project and also download dependencies as needed. - The ant script does not work anymore --- .gitignore | 9 +- lib/log4j-1.2.16-src.zip | Bin 473361 -> 0 bytes pom.xml | 166 ++++++++++++++++++ .../java}/ch/eitchnet/rmi/RMIFileClient.java | 0 .../ch/eitchnet/rmi/RmiFileDeletion.java | 0 .../java}/ch/eitchnet/rmi/RmiFileHandler.java | 0 .../java}/ch/eitchnet/rmi/RmiFilePart.java | 0 .../java}/ch/eitchnet/rmi/RmiHelper.java | 0 .../ch/eitchnet/utils/helper/FileHelper.java | 0 .../utils/helper/Log4jConfigurator.java | 0 .../utils/helper/Log4jPropertyWatchDog.java | 0 .../eitchnet/utils/helper/StringHelper.java | 0 .../eitchnet/utils/helper/SystemHelper.java | 0 .../objectfilter/ITransactionObject.java | 0 .../utils/objectfilter/ObjectCache.java | 0 .../utils/objectfilter/ObjectFilter.java | 0 .../utils/objectfilter/Operation.java | 0 17 files changed, 171 insertions(+), 4 deletions(-) delete mode 100644 lib/log4j-1.2.16-src.zip create mode 100644 pom.xml rename src/{ => main/java}/ch/eitchnet/rmi/RMIFileClient.java (100%) rename src/{ => main/java}/ch/eitchnet/rmi/RmiFileDeletion.java (100%) rename src/{ => main/java}/ch/eitchnet/rmi/RmiFileHandler.java (100%) rename src/{ => main/java}/ch/eitchnet/rmi/RmiFilePart.java (100%) rename src/{ => main/java}/ch/eitchnet/rmi/RmiHelper.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/helper/FileHelper.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/helper/Log4jConfigurator.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/helper/StringHelper.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/helper/SystemHelper.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/objectfilter/ITransactionObject.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/objectfilter/ObjectCache.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/objectfilter/ObjectFilter.java (100%) rename src/{ => main/java}/ch/eitchnet/utils/objectfilter/Operation.java (100%) diff --git a/.gitignore b/.gitignore index 0f182a034..2595e2a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.class -# Package Files # -*.jar -*.war -*.ear +tmp/ + +# Maven target +target/ + diff --git a/lib/log4j-1.2.16-src.zip b/lib/log4j-1.2.16-src.zip deleted file mode 100644 index 086789a2076cfa7c88b567f012b5bbf32732c6ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473361 zcmb@tV~`+CyRO@|ZQHhO+qP|E+O}=mwryL}=1g~=S>IW4c6=vh?X~tByM9#EpSmh5 z^U3?pd@@r(8W;o$;6JWuMt`1vJ^bel1ONxX-pQO^RRt0N7=OA^+GP+%#?=EF01)IH z7y#g3H--Q5ZRr1e+t9(#*uwOGK|=m_5N!Rie=7pwze8;7%~`DeUzF(|%>NMz^=~LU zQSXs13;5SY z@vyb|zlpOkWBtF0b9S)&hq%AipM|N7gQ=7AKSchu{;X_0{vqzK@nY}v4`~>Gix*>i zJ2T7w)Rh0@K8*G^qLqn}@jomf$=`VYUne%PH>NeSv@!jsjf3+y`hP^Do299{=|8R2 zzY5QPP+eUt|7pMZYfJrUcPra}`tSc0(b@2y#wP85Q=7(yE~e)8PM)S74mS2q|G0X| z{=3ioD}G&_{?pO^HzADj$3`Il7XFy3I#4Oq3n%~pCL90&!Qc2Imd<~4^p8zC(ODU~ z8UD*~DgPMmf6)Jbh8v?M>%1if({rVs&sCg!q#(PgtW&0HSk-ZPtl!^n%y!pB_|kTem+{j&)QGu5L?O_c?assSwo)jX<)hT1@86C(Q8;t7*^a z8kH+EpQ1ACGTo|M7=gTB0X&pJpT(6UOMSigGsTva8J*B~zL?%!q}_|MvKHRhIUbAC!U44>B}#rbq+1ouL94&89_?57oC$n@76Ty!hb}& z>R*RA&mL4miJMLsxgO)Vg+4%F%;xr}r*L(%X@*(QtCwT7pTo-g5Rp5YI=RJLwNNIm z*5Bz-r9mi{uu}5^e-DQKfNwX-u?Ck#eJ8U)xqplvuqY%^R;Io5!S&M@mURLxrFSEH zUl@=LRcfp@dyaO% zJ(KL!2VVlVMFC`(tV9tC2$Zy+-EvhxSnkoScYW$YNTnA{0T|0JZ%cteN%hO4s*_Si zW^vG3%_E4LQP{W&P?2{*_Fr0}Yt(^~t-DU916TIp_@Hm007e(^LI zU4RPUpDlTWl}tcfo=BiwL9n0ekX@wIW}VLUqstZ^ylL)gJ5H^gp1o*a`i^>wY3Ftp zGNs8!J0AmCX4wozPhI~F++#f&T|Klfk zfAbST2M1F-lmCI6l&J67Z?Pfxz3MxT5ACDnZo~PWrxuo{k<~Qx10K4F;#)=}wo4R? zDLFP3e*fwvrWAMUu3d*J{t)$Zf8B8(d;EP7lDk+kS>o16U_ZW=h?X<%sF}5BADZG~ z=%ou$7hQT`tus7 zr7}T<0-b%}L+w2?(5D8+Hm0WpNuiJeB?}FfG6vn&Wq49*UFZVJt;@Trax-be%2Kzc zGuqIOBk_4;KPswyfa>tK$p^|P#i7V>v>!N}_B*BbsDiSdb7F5U4`+$@wcQA>vMvs$Yw4QON8GMPm-^-92SsdJMV-y%34X-AAa2c(~z>{QLMCU57^mPjFUPItDd&O;;whQYpkN zy3d7)DjD3CtkNY(t*WuiSNjG73^7(I@QF@wM@GVr6CfgcObF$88ul{JhjB|2Ab6Np z_`VZ5WsT4)9+vWb9jDt`#(P6)r+aF8nUz8?XYS=>7`gnS7f7@N5)}0mf+dCxD4xZ@ zR4qCvA=41_)S*QCucfcLx13`jK4Fge1S=OId3!&0*EBrI>`{Au72Ix}HcuR-knI;= zvB%Gk?-pK}O`ZiVD4@+m2ib`Fw0`{ZR>5TCmwGr~^m^Y94GIp4-ssy2rw-MCvt}8%N_u zViuQuw(%%wL`DY04q1`Nm*65WsYxt~!ag^< zktyCJM$FJzaHgJXU0Uu|5tLKp3SiU zuy~h(V#D-a)i%n*bAFooa;v@4%{ejAXDGgTzt;I(QL zNvl3{Si_D>JVR+$up9^z^2QNhWUF?p9RUesF0^Zsl38m06kH$nwgKFIVM_70!Kgto zoV{0nB|Q68!#> z)tgr6$bI&T1J7V|%m=>#PFNiG3on$eBZ54nOVywif|uISHdqEP_R<)`@K(?S=L2Dd zp)LAiYSdiT1pW5tc(M=l&^}G)?l#ia;YjfiK9hwE7p4TBDIfM%L~;N#vOnfMBj)*i zLIOmSzm<_7^Zb_v;~2bawj=oF9!4;-_U-VyLX|$bKk&cHd?wyKXdE~IKm;ZL0R7)& zUewOQ(9YP@M8(3%-rdm1##HHFs{cRG{Thuer$4&yyQXgQj6|ha!Lf89tmA(8#-w^R zi92OUp7a4&NB{{5T2ElHvGlRm1sy;lqSWYhK!OGd)MY-WSHFL53a>YqBGEqmjuQ>k zbm)-T0ODV2WymJu+^!Ou%}ksea_-{A3E*>#m3*AfQk`;2LaMlE!AK1gTOvnPUvla* zQ!-&3KzXM2`}o7n#|;oV!6`jzn^x!s+fa>UyjlbfyQHrlNVMTyC*CSGD4wI1~7bg`hjxJ$c(#6Oc99zu?fpYHI_$q$r{P_JfqDELf z5}R;6QrF9+zpoZangIlD1*<@VmdrmKnfH#Z;Nfih{19G{T9(WFO0hhv!MyKtE#PzYs& zJRiFp?NKGjK%&sx$7pgRLL?ShI_TEpdX-llX{hArtCsPJI!>VMq;!?)&<4Piwi_bR zBD}@lp~nOZq-Pacevj|%Yr+CjS*uwbb0R)ni&nxUdk%?HuDsvOUFHo6_oSm!_p1bf zv8?*VJdF2q+ttfH-a*G?Uk=VKyy=g{0%g3T=2o+1mGhFqHijZt-~A6Ob2L{T#^T;Z zX>0~`$B{yAb{nk~R}Zj5jiyMWci>Xhm!jRrW>)d_+A~Pl97ceh@u_|&4@d9v4k-S@ zvoaZitqZUWXOyRn1t6*pv*^_lvf3%F>?BAh4yBApEZfkwP&6>sB>ED$V|DA3z5{#6s-el7wj?(U~=x;6P|EEL5ZbcG` zi^*;KMw1Rbtko&kjkx&F53c4Tjugy;Wn0OiXOc^#OR6n|?od*+y$MTfD$jk|dY#R= z3mJ)`oa&Iu>+)q7fu>56xYuU06QTb^b=BDZty>PS%yN17oBAwUa46U|Z_$W|aX`NfId+;5#?yQ!+%?qPE)&l^SfCftX#RgogtaIORRcyASwi0Y->b=@W{@cgfy9A8xmfTXRMs0mwDQ&cE zfPtw>?nritdiL&&ZidTu+0sgBz^pdxSYqnq-N;Y98Z-{#nW#}U3hqOS-|ixyhM+?c z>*XfnewcIvQ*L_R&61ka3X)x4DHoc6@QoX(aXA}Dw5^A8oUQ$orZ$$es>|g{I~r1? z*48_*S}P1(I!qQ}T=ESuHuUbl#EcpWPkpi^;Gi~r=q_Us>aTL zMvMt|*8YB@lPzg=_ynZ7z>3NByb)pcCh?Z0_>STL+8~$w#E4U*nGqf*U87R8WWnGG z&!)>$#~ji<2gz*Ia8Gs#Y(#=9z828FvNRal$qi>%kKx`q&8{TNxl2ObB?;4d_?D+G zfuqbi#6Jv!iMBL|#vUDdCh}sul#2H)++C7eAdG<1cQl>F)N(F2fZ9;Hdu81g#uZSv zFs8g>xVgFMy7~Z@6LT(x`W^XQk96{qB+7vEM<%UlD5Ut|;Lu{xN8x`)g-$>j8$%BH zKt#sAUfKfr~B9 z@}2?MqPgl#>)<1;|FFH;SNNQp-Q+(q`28JOveVhM*DOBu;(FN^cWrIlMH-_0xK%s( z81>DO?5hc;U59;LdDv=ov~3>Qx<`19$j%T8pn5(bb%_8TtwVUaQh90Ngx2cy`t0oiEpg1$#Nx8R917r%64^tH%7L zve235pxe}QHg7lr{Ln0}(Sq#KPtTX?OIWu*?0bZ_Nj_p`0cBy#Y9oga-f87n6Z!2$vOAX;~YaU85a>2qKqS(7NukF?$ z+yEf@&0922;Z41;=f!?~YNE!(3{26=i|p14!&de_tUGvLe!~8{?;l)vI<=8KEsTq!TE#CUEO6gkEV z%Dl=EylFPGwH@7~eC021b2oH`nTi;Z{+H>jjMv@jaCF@EfQ)Gc&FKM42ka0>Ql<~7 z=S|i@g6#{flR?Rve0I6p4}mlb0csFigDO}isnQf?CspoFbubZxP{Hj&CpJI{MBd*% z4cWUg2DH*4I-Bt#E7%uoFvo0*LPayk)sRxh9mk7>AQk7MY%E6g2xMuM`JF4f<3>jU z>`*Z~<5Y&}r1FWoxLyr_Yw9JO_^!BP7P2@I82$JmL#n%EX)r5C>2^MwH*p5?8`=%v zCAiCI+VgMOh9~8!m)6=TO64%xNx0Ss_;H9Lb=mnI2jqY~cHw~z+2i=%ZH~1ct{gU* zWQe_IB3VWr7>*poMrRktve#Ms@RcIg5p{jmmOoBb@I*voOpQcuQVR`Xd*t-|t%?~% z+Nr<{iU2W6X_q6~gwt4jxdYOXp*oLHN-fcA$1YfK8OGZ#9v%-3A@@dKUyt6MxFii7 z;n}0!Z(KNar!OwyMjyrBm~!^u03ui`gXdBXAj@SZqK$r@@U~yf19^wm*@kB5Oht<+ zi_$`c2;I(8v`U4VQPWaeR}5quN_C{nYESm&MnS`J;}U&D1pc@LzAB;H{bap#DJQnw zt|E`9F=1K~6;vhXe&7#Df`O{#JZBh1ZFnyLF6-Nxcpf4L`V|c_>#csu zXLk-P94Y@sVbw<1j_s3pcTv{4n_9#Kz4z6TV)LTAoilZe?qoRq{ne0s3E2yECK1K( z2@Ts>YT)$MI?MJ&5EyyNeANk}=tM%qII06}??H!0Ox|{!9Fqn_jcPG*6pAJbvk87pQSG zyx)abK+?G$8=o++h1d=H<~1>u%HR*$M3L@ajS9uMx1`zELSYQKAQ;MXv9V0X=gmd; zbIZ^&Au5R-BX09lU{9M{AmiV^1g-8q$k29RR1esCroPTyvSzuM#OKIKK*ta&di)Mu z0&buE5pLc;>65*Wi+~>YfW7%aGGxz^_?ITu-YA?7h~-Aa2aLtH7!J&zjx%?4erImP zZ<}&FY>%Fg;_v;QEpZq3@svUpIBd*eT~DDy_6PLl_{D*X=AH%c{*t^@5>8##Q|p#o|B zGI~ZERwu-q0#V8mm4aqsTAHXcWtniKX+q0{XGLg~aGWu=t^r}}=fR1Y{h~=gJm8bD zA-e8M_{fPldB$W6UE*I4P}+Vgy_qR4L*{{?O`sK`^NR_x!QQLNlw&SKi$uuWIm64u zOo$vaFHM8VdhI&W*-tT)TkK?b7SZ+v`+A-ZTrW+A)p9l$d&j!RtW5*=Q9HFf8#SXm z9gUqi;WPkB#}P|1cUt7cWQun`j(+w><|)o&2PdSmpexo9Cv7qQW5${1N9B?x4H_PM zI&aC4oagW?C=NHyYaZwJ(<5bsNa}#TKZ?@O)ROk7GCyDMEI0E@=FIh)^k{rp#Lp~}Se@X^xaAY}lO`@Gy9J{%WCFge?=vp^)6q1vUkno1AVcoKOFMzc$X9B5M9)v~w z!f=t;M8`>byHouJiq2CbLJAQBi!7U_eePwzkUNoD=#>)sfd-?Z@#d20cjL`xXWksg zcq|meKH!GI3uIl4IX3_ScM*km~|GT6zG@*1fBL@deyWW0{G9m{?|#5fgsNiiD4K{OFK z;w38_TrR&^njQ+(^)k3~F#wsg#iPctGpIanHwVQ`&hwtH*EfJO-eNny(Sr$o7=6Oi zyiW;1H#jvo&-&_{^X+8%hu>mo$2|~`=vTbtF0sGQ&DX5ldN1U}Nt3VFsx8j%u3v2p z?rZhOcA878nCI09xHPx2U*JA05oF}f!pKJ5*P4e37+A{x)+wzi1Brx zmO_uSM&Ne^j6Ix8>dbAui4cc72F0UNgE7CEg${ zvz@*r|CSq9>*i1vnZ0tG{y zA3hnZx~o#9@VFjXp{vSn6S^l^Fkn`*{KHL)C(X;&6~r)dQ#vL*Hg$+WP(JM$#D?zF ziLLFSDLTL6hYFIJ-p$j%qTHi*!UBm3fVM#{kN2Y1$8d6)lxq;IdfvPqtSViryfzx+ zWiLV(k(@IMPf_#BU21?&ftm#K8L-w^@HQ({fO4T@;4O~5r^BvuICpNZ;2ARx%pSgb zsr`CJx2j}?I`46t%3zY*{- z7sF+K{taO@(GD+Gbf)(UOAzeo9@HieL=S{hUKd~t17dE6?Spa7CwEK67zzraSh}V| zQ3l)17}eX6FA^1tC$I=WtO5pQXDGJo8j$U(WtOD$G%L@T5}QgD5|6|4_Qe1p%dk1f zDOW;0`wZ>UnH03Z?t+Wg>6Qf@Mx9yP^o{XgOi}>?$a@T58>C+9A7E^>Kr(W}g)i-% zcfUoaYS~~(e8Iw;B^>nN!MF8#hq`YHXpYQ-l(8`NCTofOf66)COVn5J(CidID+9vjmzqd5) z7r)*7I6v02nh84GsG78*EK*wMb1-C%>yX59~CfuDfydngs_hfBba3*&L?LB{>M9k+Z5W zTwXX;^voH0#)UHb8GOQOOxpZ54Q0!+Ac;qrK^>$rrjrN1Cx8|K9GzrEEP2j%9TkAC zXvY;fIdMPMk~lA)uy)VdX6`bBXg%TA$hOu|co>l|4jm(X&Kei_VDs8(2EC1>-X7RK zTslMN4>`Kv)LAR^%6tUUr{ijFi&^`%7h*$T{ZnNYi+0P!^=M;gqGL|*^|8q_fHe{? zpwgtG_Ir!7(qo4LmV}wgCyh6S&T{H=_t`>h@fv#2tJRYx|W;m)Qe#$GIqhQ$%;5e_`58r`A~^LpWSOEWZ^h<*YXHVTk{dv z$@~t{25F@l*{w~sJRe`9$n{NQ<5}&+NYu5a9~_G5H+mGfN%o47QKAADO$Nwk*IzcCw~IuM0wHH}!+EPmsYx3&!)>QiMI zK`8rgKoz-cgdM%)(&lvD2aA!U`U{-TeULOYf4&jICB3!j*nnHNNhygU!mW?O&KL(PGgRua<+?3A3#n$G3x7EZ{dCxwJ;a`h`Gs@JV9+@xA1rjWTR9axF1XT~`{h-}JGdAWd z-@bDs7k?7|Q!npbw_}LAv`wN7-mwQ2Zw+B*H%DBW{UrN55mBgTQ%0P-4tQlroY1#H z0Rvj8&%zy2`E3G?uquru110=f0Ll>9X(zn z2Yc%?c5KVy^$tYrx*dW@adT*O1CF*Kd5g0|D6h3Cl_T-9`0gjccR`2jatp2)EdzWw zXoC$hf*q}Ktofh#V1#90RLnM4ysKiCxg4>?0n4WisI^$ozCZq=fc5O;4?K^Mp?nc( zKn?RpeZl&EP z-LxremN>*|gr#Y8$v}}zU9X1dn+TUMRXKex}qSQfJHzmg{tMx&Dqciv|V!gp9e`iS^K|Ej$D4xz2<{|<^WOMOq!UeEY7q5=7|2;AiU@4@G6BD|LO zXQets00030Z^0*FY3gL?WNhL2-zF|@v~BD^)pq+<+&C(swP9sYt0<-c-rVHz}>I z`rTS_UybmCN>@#*&Q)_uq#|P#=Ii5TW@V&A>o`>HaeI<1yLaje6|hphLzP>3rvg-< z~fZU3Pn{}FwO zgNY~-mzeCbE*wnS z>TveY?_Qebmi;w*S-rriwpXehfCic#&`!}#V$8;DjV}Qc^}vG@r^0Gw03c-O%9E@y zq~$T_{*GpqqQ{KR%$ViJ$Bh*mApyVM&iDJ9U)ax&?_KU^YwYT2%+D`5ogUtwhnJ_9 zH>|y%`LlKYigmTM`ZCF2YYwqr^%XyW@-vXdpR-46%<=j=29 z)h^V6`=E6#A-24zJ0+}pSWro{Mi6!}mBZCxAhr9SyIUub$S`{a0TgofL|3E@MPN+e z6wO=(p&)9LzpMw_^_s&h`ij7N`0KK`YjT5m-9V9*JMbKPQ+#! zQ#AM3R+M~8YIFM(>7kvWi?T1QpzSs;q#4+0Z^2*kko9Z4R0TzU)SUEZQr`)Gnpo-X zlzDmkz?}3F9|-pJl%kuxpXtxV#>EwYl~%6QU9ibwIZ(Ybwp3+4z1w{6m~K?Nj%4F_ z{8GQ0Q85QJ`)(h$dpK^&Di94v&!VdKO|rw&d3||NzZvk!DPLC&zzTX@2*sZ2&BF^w(8w~Cu z$h0UMv(0Dd^ypGW0F9Jv`X$|}{Ust!XGpHEYH#~8UQ9s@q`DV6j=1)ILqU`sG2Z5t& zoVWyITrxukh0xho6;JPZjZa$FD-!!t7Zf9Kp_7l1KKAfyM#UD^IVnO_W&#Nk1|jg zj7xFNT_wT}GOgsSnU)R;1U@*;Bm$Lh2`$Sh3yBPaE=xL)F#r=pQ74rIq(2-P{wehc z>8B5~3Y@_(LoNGr#kRh=k!dR%*vInJ_WZ6(#PrlzT22!^D%G(kC&Furuu^o|Yd}kp z7v6!-J-r&xVG9g0q}A3A3tIi>#Rcww%^ROB!IhNmd6!Pl@3vA+baPR%Z#;ZrG22Y- z5EpYi{?^HW0Lu5YOYe+`OUzRdTqe!uKZ`!fC)xz%T)POx^oynC!Ok{o5!Run5%A6C zAS51ECQQv50Tob!3B6))&}6ur^xeTsL8cO*YNhG=DUG`Jt_yr8u5F_zSefOB>jQJv z&b=E9<$1~zK)IEmzXSQOwSLKIFR5hs#p>kGdvg;#yrhd-JmvDJnku7o&u}u9&}mF7 z3@Y8iz2T2zcV^dS(Aw<&9)BqHTAAUQI-UAWr z043_XXaC9C(H?T|fi(=;SbEuL;#n$M0EGtrJ@d6jDLyD&Q)}OyKT6Yn{QLQe?+Ii| z3+TK-8?ho-m_Z3mURWJ_rxX2Qo?Q25iOu_Z!pysmTtwA@1BP5OQ?V?~#(1J{uf6Q7 z-Ov$huYvXP3AA!Ud;Orye4M!2R0+NbE~WHYELM0OEh?C0d=GnEIVmwY0en;yVeo*PhILQM z+DhJa+YRI-6QE*;E*yVTh6AZ5`6pbheoB^;r>M#54I7Tsf=Cnu>blQ}PO7$3IV2#c zUK|xr2fYDqY^vm+_c(<%Y`&fl z@e+nZJaajx(f@Q{BJGukU@w91anGNg8;;sX#ja6toop(8RuvHOI3QoLC>1FtTBRUk zun@A{etrXRhw;+<`2{&mOsQkEqTX+At(N42_YygH__&HrNvn`Z)z-sycXui=azN-_ z`TjuSJQwk|*b)A9!AU?QoQ?f30^T^>U( zEk4w9s0kD8bIwtu;o0uO3p5*lC4K&IIhNJ4evz{O{>6EpjoItfvst=>vGJaI3-z7Rd~GY<)c?Ss(sTo-0l_fM5I1gY#gK(oPvJdK^{2_MxLDf zMK^I@;Ia?KsbolSYVlDw-dR71O{q`u6y#ihK%4lvaau)S+U*n8$^Caz(uO81!;v#5 z*7SEy6j6nHybV6Sq0=%Un8Za|XFcI6ed)DMKtKUq)lX6th~`g+!Pd6unTDC=jS6wo zGeH??xteweY>`CeZYq?MxN3J)xF1Vg;CliufeiprgB0E{lczu~AB{nU*wz>q=mT^kNl z+uC(0z2rHEVC3dt#-D5rm;uE0&G5mr<{Or^{EI ziuUKarOURB8{^fQ=ASixt)q7DM{-sDl?v}!=c=G(6{)J)D|!;$$kBfk6yK6&X8@(k z-^jo^^GTYEGY)MvEx60?G_ZUW7c)Y^Tzp+rG) zGHdD2f|r|>T4roF7fiiAIe_#UwYN(}i)x(M1$W)u3&kjC4mScEzEe3y z5-atEl(!OSSFfuuz41EL_d8zr;cVCQ|K#y`KYV!ql@|G3+Kx&_1OlLoxd4%r4i1@Q#@ugA zCFPUA!xS|l!GZT@RIi^bMj~yc&0RXVJ;uxb+0yWYk8K!|Wt zU4%IxSwp`8GHGkr*yYe5cFsaE0i`8^?!dBFE}4}p!#?-ncgdgVkFVrB`Vdsdh;@H* zUFP9D=c`ys?t5X^mbr05AG#Cc_`2zb(^3b&%k|4*W(cbQ%OL}p6xK#qk@GPPy{`N_ z(Z_-wo)>0IMIxj|8W3(3&244bxe0i}ytzsQcXEwpBS+%(hI(v7t~wY>fz5#+yG0YQ z23rm)R>x<&{qeSthyL~iR9th)WlY%91rNqjrW83B|J0jb)8e+ZXiDU{e_CXZx5r^g zow}3aaq}g-`)8nAR}_GK&adA`vF`Yz<{i6muD3N$oHou^=0cyYURjy#83n7*@FL6L z5H~ZV9QDp$t?X(0@VqQJ^IN#7-~*z=ykTq8(jbrBeuhlyJfa+LfyMS<^8H^q+khh5k;zF6mH9TE^L|zY)X^z|Vvb+6 zMpq)LSj|_%D(YVEienV9qKZahrZUhW#?W?R0 z8JH*7)wN4Yr9W%|`kd|uZ)IbDxdR6y6WY#O;%4AOCDkK=@37JBS~+CtB$jo%ciRFp zG~H(Edv|OBTpF<8{BY#yzw<{qhav$OY)?|&t?X*xE`7m&rNQ|jSS&(k0rDluK@|D1v(3=xcv|gwN{H>4*O~q z_VPHSLDqChJ+E2w*jypLi-pp56gFqVlnujF$5#it6E{Sg#gG*qh5(1-8poaK;!CG6 zY3`1OKG3b(ElYMJJpco3Js3P^Y%%x%{wBOWL=0XyY``0^IvRdi=n~&Nj{aQZIddq| zi-=UDTtp~oNAn|$fiS?=@Bm75cFwg9IAkt8z}IeWJ&-NJmJ2CR*O6lV=SA3l2c-fX zAWL}sR~x2!G3QOFRHRS#IVSa;gk?NxKOp{#+vvrCMZXM z;(8xl9+q1NWi2M`dde7<`a3sLZXq!GBpr8*Nw}xzg+a%S`{67?DBXBDyZxA4lfa5V zKq_eC%j{9n=RGn%!kCmitP5U0ki@kWO1!|a_-!0}7X<_N)(S_DSL*V;YyJIoPVzGm znxC7Y;Udu=ZZe}LX%N-MHv^3sO9;|_tO$(I2UxU7Up{iQfjwnGy)?X3Fq>g zIAy569hx|y91oACxT(z!>uS5((%NN4e(`)PM7QNS?U!*}E|ex81Ba4SW0txv!|)%Xwb|p8?RGNAf!Ex;;TH{sDB zNkW5>PZwDo&$6^n5MhGV??w?e`55ezB4JvyRNcxu03K!ZFS-a{z5bNztclBo+s^4a z48Dx}2K@KD=j64vP|Bp}SjA-uIZE>LW;_-uy z0lQqUswT=c^xJLbTb5;bZIY#PG>O`y<(qDr5l2;0j5tPh|MELiNT%43+}s$cjbBd{ z$=!=P3*Yu$M}{Ba#;sbe%%F&-NR`4dXBu4_o{nmnPT^oQ^fn&X&xGSFLe*DxFk^ zf{|gZN-mNLQ%YSk>6{h>2QgEG-bE0hY1=cAm^ZHu!!v0@P(#lSa+Al$Mg3Z^3>p0} zxhE;HZQ=cfwHN>dp4*;pfT3#d^BYL}h8Kq%BMUuqdXwm4+?5m(tt!y7jms#oeg=wE z__kfn-7L+W`**wg51^1JeyN(sSSsx#o6?9opd)>K!%yM6o^S{@q3kS@0cE8RjwzHZ z9bNP{L(wb3evZ(Msc2rH#_oOu%v35;A&*YL>Vf1`xa1O1lV|jYOoS)@gHzs55f~>+ zPszz-v{t#J;_SdonS60503Ak4SSvTnNf2mJMTsV?#&XOA;*b(6S~Du?>98WKg7Qq! zKqPAdI$BYsA3{oVGCGR_VCiKIrq$aSB%6j{VwQbwqI_Xo&9FeANxNJY+DR$jq2#p#0EW^HQzV=_U?ddf;F8 z|4s}6geGYi3bxGO z7Z6#mQ?g*j&kB<+Eh~eoFs(wT!ra>jy8+RH3hcvOLSHiIBw*!8sFt#+(SV2` zDt@1-5xV~_x4Pj9!&KZjZ3##EwbMvK4G3Bw<2M{L;rptPx=)6bXtI+E$t(q9bqg%G zaB^s`zOa0%y2^2z!y@41HyYuHyU=mr(NpWtx3M`)-&!^Iv}bK3lRPc8 z2WgVhyVlLd)44D8Z=S6zyaBOc5(o`FuSj)h)#kG0Dn4_%9FNn``^Mj;P^Z_zjN%7I zlu`X3v)Lv#Al)rhKRI-nLeb7C0TnmgR;0SJ?Wo~<>NBA>g3t5U7Q%q5l*2{t1O3AJ zzEz1md>M-G~-J=-P=+{~&=n`aq`TsPY<^mk+u z$+^nSqD4?di4nUkkFMef@Kc50txbCo;i{OAjDod+2+Y{+@YJ2v z#pW=K%+&=ac@KUnoc2E&J{rt|a_wDpVEE`o;pas)%OLT`(CLA%rf>ug#2R^0@YRJQ zQao~}t?isp-C2~Do*2#7RSJTCrjPR7Mv2RCr2`Y!v-}wLMAa#gyN0l8V)b|)2VEH^ zkzCBB5UHZjcBEKGQGq`~XAKCYZhXH?maRT6`Wacu+EXLL9nE{oZ^e(HX7-%1IN+@` z%DAZraKxDg63YVu4t4de#v=^61~8)<3xF8TOD3XL>>#Egp{os$1j|)&n6PgD8-lE_ z66E;@bTOnUeJKZ#RGO6OVX&NevSjMGLRNX^m`vVdA#oAR`ByU25%InAQ&W?gwmo~? zorp29Ys}XMT$pQ0y)6_T`=5l#t^>{epR{?MZVO&iup#PlZuIcL|HIfhbqN9mNxE#? zwrzCTwr$(CZQHhO+qPZRUMe9_+Vf;9kdWRU6ti%-3%{wlKI{{6=h9J0!Jp5 znM826JkIXo))=u@Y&drVMzGy|)~(zN9X6bHNuwMvAEpUZaN3q^gS2YJDyZSn`;z@s z5YCi{zP{Znkk-t_f%m8VegEgwhBzg~-Q9ejfw&>7UaaD$GDu-w<# z@!?Y1VPjAyhqpF4HxbS{Gzrb73fia3;+Z~Mpe zM8{tB3;Bmy_x)IVP~l)eF#es;=1`5LosZ8!s@Kk9dfVG!s--w6>L1u%XWX@2pMAKs zZ;Omff(wD*fKc&Bm7?9QDb#p(Fhg$2k*jKvPY|>hh3asj0m%A4^+B1=ScA>lG8f^d zyR{WQu*6PWi;$O}QC^$s%@3e9xn%^6ELQgt?5GxELkndpE)r+>Dqff<|C~F=iCM_H zb*MXnx*x65tF+i2db9-mZ5xOyKB*|zZ0H_MI`LRy#U*qu!Uv}hu`~FaGH9LlISpyW zwl3ZQSM2<)SsroEvg~sAHj*n`)rgKD{Y#rk|O*>fWiR z$HHXw)4yI$lcZe;M))*i%l*8x=@Idf#U$Wv5A2q1_&SlaD;jeU5M5az-1YfHM6dY$ zHp-cWe(&Ae$QZ?C>a?tLnwfSuY7G?oxg3Ujc_$npCMLp8xK$tz`8+~Q84aY z>+nVlM-(WbN^EHs>G#$9)x&RGQlQw^5)tGS@pyFlxn*DM&n&#|TO5@-VPK$^=1w<> z);!c6-B;}7)qza^{skB@Z^!+2aA<)h*Y|$7z@Pi-s!`gBSz@5FNZ-mI_h!k`PbJgI?rA6v1WybRB6a+pZPfc!lfp>U3C4yA#!wZ1C9=&9 zipLMy!YQFRr;;_xIo5I!h)cr1US|FpGN3FmC=x-i!Ut}o8Sse-zVj>_W*EB}LKPb$ z1aq`R3u6qyLz|K)F=v1p@C11*si4iCc8I4oOg{5S^O7)&N{HUk&}UvORui1fNf->< zldwguqw@pcW}ggm3o7z@ms#byy6HurM(d8JgePgEXmS>>nCX>p)a{n)Zt7_ za{j^R$0nK}NxSbQkq~Bb(L-hm3*boonR(*z@o;a~rsI{M@seQUZbrh>4b-g@8v-`P z7W0WFE@3slx)Vtch$W{cRMMYHL)K?Yl+qT^B-QlMH_8a;Jlg1;H+@ft?vhSfrHLcx z`UE%NJj6a5rF9*cT-W5*vm9Q(Pk`StW>#EHn#Vr=i=>%-97)*fne zEA;zr`ujBU^4=?-&zvsiBg>o&unwb;N0mR)qTWbDpm^UUn}RPZgfARr7LrZLD0QoX zMl7$GLMs?^RT(el-9JpFS&RV53~A=&7C6Tz;A zC|%lvG(hcOnt-= z@dJVy-uejUbZ5&*2o&HwAy@^@!6Z2H%$zNto!u0It${2Y@m(>HwlF6Agy;FpkP?xa zaGbozoD|5Vu8;&&^WKX}f!JEl5eNYQ9u0zsJhsP7Tv(>8oXwr=l)dZ^m|UdU10-n za^JCDo%=j+`>@l1s8w*y4Cxb*Q7ORQf^bJ6x>~Rp^VhZ!l;-b#X^aokZx4cfVo+L( zXN`O6sR}eRK1?Koxp5~?2!1bB#J_94qMKKD(1|PyP=C@Z#7|Y+YjnwwT>TAbKu4b{ z($N~^0^o|V+za>kKoS3FlZ4af1I)`~QgDZzkn3PpP;!tbxRf2=d)FhNHcwc0Q+QVm zw;tw(nypF1?xK$y;tazWWKxbi0Lup<*Ii~4FbztMTs69vg&knZIY3;7$Z@P8Tnql2 zjORdFtI-@0xwt%R>slN^C?;lUB;+^#Ukb@IoR6dx5+w54Md>0)9Epp6Hsrw*AI_MP z`0OX45MxB>@zZE5HK)B(1ozX4K*jS(gjwOG2f-1<92v^wl`F+bZO{s zFNDmMM=GDPfz=Z zMSV8ZhQAlpM@KLM+>+e1Pb?l>JF;1XYz9^IKDqDc_+BsR5ZDS6p$$>5wqHwnx`UiT zzwoa0M1J17!CL3FvlZ3FpF)1nGhRp(;DA?tuBoox*a4=M1 z5IxvXd?9n3Y9bMe%nX7ya7l1NT}w`4)QHz?I6$!6>k-uCS_JtI71-bFa8fO(*c`)s z>W!u7H^vbN{)~}>I)=iTRn|_vF5zeLG6oI_Ch=fhx414thVJgKd4B%BcHiI5&qAx+ z%sf~s$Dqlke&EkR1faCMC%G>z(Ybr%bS043Th>mVl$-!Z?`4>7ocpQg#SJl~1x7NB zzEzq{7SZ_qgA=cst7|DH_bVB~eoom`NbMltD><@^PI$<))hVsl#U?iiQ^0yX)4P-I za{v)F(F<))e~XKmV?)BO?rZ5Oc_8Dzda`&BSW^s<+75&Z{_s-u$>Bc1=PtOGU7vI@ zz-i*8Xu%S0*2TxXR8><&RzYz+E^wXwof#F$S*hXt#&R0#1IXyEn)^&&J^l|M_OqoD zQKl7?>t@m*k@-NR4ut-wg^M78@^&o34}TAp_Cdx@!d?ncSl%XrG~oIHfZaR&&0&%P zu;VJsbNy+zE400S;g9_+2+G1`qxJST%=^7ig*wx;)ET#}RnDGmIH-gAVLeAfsGj+T zVPp{d95){MAb~Gggnj{JcOZs9m#Eh7ul{;$+Nd$;`esMrZWF3OGQcbWsz!#7r>2vKO;?UGztZtaT zjvf~cv#0i&zO1tY;)D32)#TMbD<_%BYQp?jXh!5Bh-qQgiO4M(W;RSuD`)YSXCJ5M zw#?VO;ST5dwimvVd6@eeDWDBDg}Z6(kk=G1!itl7zZX& zQ!?qX>IYZ>qeQPoMvVxotbis%_n&H8l;jarK{oAUsGXk%*vWarXZOP&e`W+OP!)w} z%R{2ON@p<|JvURT^5Bu>g5^(j1(>ZN>y8V6?T?CH%;`GUrlWeJuo3w^HE$qai5)fRuY!{N6sXp#BjhNPD0yQh-=^VW$=eUqSo~w7oWKSU zg$~E+%_!4`^3hGXqMpj7j6?*L=1JBxF`(%L!Ca2*^&(R?OjTh%wR<$*Ws}kR^55m9SH@OS2~g-eEFA#0oT>n$eS;)p^N~wZ6YmQ&6{?9 z)HQ$H!DM4DrRxEiN6_Tu>el=&t!DI(45_We(?gjkI6@S!5C%NQAF@F$ImSwmCbCjS zSfeG?L2alr>WxQpBadXMU)yMB&#qM#K+lq8J%Z2aN6oe(PuK0;ob1pHwv2+CB#Z3O z_ZRobNGKA!$((#o$K(02O8~dZDyn?hrdf^dSq*6%#Qp39FwOR`6f&-2X83$G)*=*D zlBoHMc06^gXWwemMW4O$IM|}Y5ncd!c8F?U6kH}EJ|_>ebpz<-Q)4k;HMpl^YWps^ z16z7Bnz(%*snf=#P`hU=EGt~;H&<5}>~2z=Ie9j-A|IjIfjoKSlh>226kywJ)u?6O zx6d8NaVQxThZVx}_FT=-X26Z@F7^nX>hjVd%6x%wIsL@fb%{u9DLFy!GsJ0nV>cjhn^^5||u2LYQr)+%u`p~m%cf?8j!p$XG0iOjay z_G?gJ``k1x80ebMaIQN#8x5RB!(!<1!Z+Q*QBWD(CKJ}U9a`MDa2efQLU4>q<|`H!jt@q)UbG(KPn&dJkj6vfaoIBlg8X8D0ZgN4_e1j*kS19a5RM(xhOetw%0%V-XRx{9HU5EIgY`iJIRa-nWNF8Z3|V2`v?kd0=&bqw z&~yUS!dnlln)0@ljN;KlV^3)=4^!a<<=Cl&llnH$J*U6XfzO+%R$|Tg9+VbKc@Dof zC-U7r9lZFvy?nJfOYRDIgYIg1OF!4?7n47#PuM4MrVEz*g(3mJUfp&bPL9WF0WI&& zRKzuG{Cu37Uoc5^p3KikB?A1Sw&C57q&!z!>*mz!VI)$VoJ1mP3n#pd8=X%2CYpX= z_3|ggf!a|#aS%d*+nQyXu5o>W=`)GVb#ZUk4WOO3{3f8AxqRG}b+ha4E0fZaZH*dq zV+FicG)rt9vaxt+*ZBUrO3igZcqyc+WB@uqU{IV(fa3pmfiO%-h1AtI1QIg2|aK#SlZNiVz( zbWQ348P>85zUe{^*^ve0wffpfAs>6gI4A;RSDd9pLrEkZ$U6ZX9RA(*%DehVh8Vyc zDT6(P3vx>|0olD>^mL+jbt4M{P3V3+e{bXFbiLi~qXiyB(cW=Bkrn+P%j&wIF-1Kd z7t`GI3;T-g{G%9tQRy3<=YB{U%s=)c0C_?62kyMV_f1-|5-U_U>mV z8{7_Y`Ny_IEf^!}vt)agSD)ri4a3k*uh8?I+S|xyWegPgafL4wlBXw`Eai>cJ>sxq zy_Y+$6*du_qG{_-%xN4(KYq{PW(*yU62`g*@vI6tHjn!g@zon3WnXGpJ%Rf(JaKIBczsr;77 z1^tBm>>GYD-#dFdKZ19y;u3KI^piq!dD^6H3H(vj-_Ms%B+qanXFPUw^jHC*$e@jk zK|fG-ttv+if2eyO3KA#l19;+rkLZTQ2ZPVE<=2Jd-+|lHiym#GFlu`dNW%@#iR4un z9BgxY9}k{#DvQ+w{POVluIm0EKSiL%n$+&IU9w1I+*XrhbG}L@z*Sj>jZu5$q1>@# zmKqk({O%|BaET3ezcTkRwckhxTnFl<^Ebgwt?N{RMhSGweJRZ%l$9I!NS*qb`zNfd zirz$kw8a@|u;bA|Lh0GOI+VCwEeFg`%Gsr5HxX#RvUPEHDnH7H4X8|y;2aIXYS;tf3j*|)F(;AQm!yu){a88-9lnYyx8C!#I9 zQ2RG-3}|#J5j*a-G5wPlVg*kIQxSYzZGUakZd#Kue1Z%q;K@1f$*$~m!^$s!9hdV3erlnOiT461}H8%Jrf9OayJ+)dw1AG_j!Uln*(r$Z{&F+*K zMM%^gUhHkvqK~NvU%>XW8R?T zb}BG*({&S@zPe0}!l%4PPM9q59;HVeE%Dw$X{m4N+Fen`)ikk_kETV|-Ju7gJGAQ1 z7eB(E!FuAUt=01E59@68G_`3M*1X5u*DF&t?~TfX@y}WAN+clmrOj7-7!174{_Wcx zDcCW~_u=2@{&^`Y*JpVtw+~-gPJdg=EhLmP$hUC=Njf!mJ?s_hSnEj(+gq{&lVi&* zxp;gjf&M{o_MRi~72vS)g^FH=SXhrKB>VB|OeQ z|B12Mt8YL3iuBLW!S;{a{a?wzvcf|DV}$KQ)5-~(z46=n2Z}}sRSA*zamU57O|`MM zB@xHUNQzDhTg}qy3XKCqqS_S9=}aKg;iB*E!d4&8TV2++SBJD!1g;d&Iq#Nlr+>i> z_$9~F$|xztw34ch2O zr!GcPW;GRYiEWI*!y3Dr+F6>VDRNP1YjPUH=cFn$k}|Dzg=%fP z8guyKQbkFHOQCfry`qM=!D(murgf~xlZ(Mwlw^mBX{4jUQ~2=qf~>7xSW7Zdn%M}S zpKHsWsM(k9(C7p~|^E*mb_n=}!EsI?B ztl`pz1~d5cLlgQTS<0yU47Y;qDsHK>2>l}dkA||!&stH@iY2OruG4YkyTbb6f)>T= z$LnK;9{cyZSZaD>aPLoBD`+#126c{+K|`*7M}%$y?1rqhAC0lfpMwqWYDstiq$O76 zst1ev%%u#h+?~p*t%8rOjhmI5^kDDqPA{jYIPRB+>!GlwtFEdkEte;OoIPzl z?H!#R?V}|t7uWBmkB7?-!)lo3P><=d?tEPRj8qoOQtMs+()xdX4j(!v{SFWeLsn^k9DMctwxeLojO>|j}Q|KPt0 z@p?5*9QKLTUWuyELI%Ywpa6Afe>k-DN?a5*(j!TR?eUq^O`RXAo1ewaMpsN4ZP~n9 zylfa>gJDS`GBHxHWZ+n<5R=U&0l0Zk8lzYvHrB=cfmT^YfJBfTOw#PvB+EQN%nj5~ zUJo8%m`0%m%L{P1Qph`X4k9Gv?HtqC^~&x1Rdk&B=44y~ z&imZ+&lkP5GB0y}NZ`f8Lp2X26$xnt7%&XaN2N{uk}wn${Swq5QeG>fVi=)f!?3J-QLJ zF}YkV!=wPzYcdmqu2qiJsso6_DUfvFwLeX^a};UbpMMKmZbq zB?e_Fa1|vsb=5^5>@9_pogYTd8``y;pQ*%F;=tU?$qOS-whA`~(kCE4rbxtr0`4_T zJq*(7fArlQSp};55!h5Cyvi z+Bs%Eok$RrG@%G>^5KkNC@Jz60#huCkkDpBzX}5&jstw{pk4+47G9C)Ox}~Jj;M7S zOdN@iNBCT~LlgJTE)sk!#8A5MFluJM&uPhA1u*Uim0U_h@{92w&c72S1pE*>O1sBK zLh6oFP&PYg$YPy6MkeB*h*ywIELxxhkqoVoI-o`}*ih%wpGlwm6!QbWQYud-6OS^y zBQf|8V|IsMLck891vm~K5dJm9kle6#dPIi>49~bfO`M(!4aG=CXEPt#7y@S-0jbRZY-dgCOkfD zRDZCBASfp)^kH5>07SqjdF%H?KLywoMfB8OE_02`nK)lwW#tz76e8i_#&0QJp%OnS zK+9W@lSmm&QC*8Wh*ib#i|Ru0ahlzt$UkjG#YnvIlp^7IGpW)aunOFwa{H-3r)Ac3 z$6M5qZxV;9hh;=TMuq6XxQvoJk`$}sqU)j((ER%$Y06Avl@&58g^KhCCRMLh3j+>j zB#&TA4NhuGU<3PSV~vg+W;wFuIl(QC6#BQKQt9%QCHDHIK|LX?zwxVPDfGL@#et2F z3+YE2A3+T9Xk}SWwJ-)lHO7wJtc<>=2%f5sB5F8NpSptP1`M?4R06Ce4otvJe3i7L zIp{$`e<7*PgqNd{5eoGmdu$~_Y&yAG6mWD|;(!Oo7vr4iMH<@IEs21u2L;4CVcT_I>_hpS~V z7xZEaXO9)ht>GkovpQUx0#$8FhN^$ikD=vGmH6qD0Xoy2kjWC?j_=6ozDYe1y@s4r zt@fkz)*spj?O^Ea5m0)zLq{B#gK}bwm4jgH)PC3oeQgs6#pbfq@W6P3lOia6laOB3 z0<|b?S*Cfjqi0IoAnTVo@>{7^>Xk0N%==hMW`!rW={n{u^yODMRc50RcC(B_Y%aj1 zS^wD!B`POEYfTy-305`Y$|MROkMcZ=ZRh5P5-;Q$SD@BOECIO{|CHlC@zc7^Zh*$W z$pH>)@Nd1=02zF`)`PoeJ>OLwtcC4L!T0PSg1XoSN}fCYT0p13)Zs@85zduMS#pCwSycITA3h^BP$cVC3c@E7${`J;7R zE29#fD2!jP-^?aNVm3@^b}};dp6mdiL?oP7mnXTiLNpSCvd)pVsZqDdX~-y1KTFWA z8`|EZC&khYa)vYX;f6I97?B&&d&!Uj8Mhf31M&Garr;ou7S8z`dTq7>5Qw%=~F%-=dx_ zP`a~o^Fr)JZRiIQsEAj7%G{)9v$p z_!@a4WS@gQxjwyK92?fpYw%mFky&{aP{`OC&zsf$=aRMRl|dw<&skKIJj4WSWc#Xb*xg zrg$)uwDH~^8~w0OZZ~JYN2WBQKteqOOn;J6p{IxBUyoMxi$0H!G$PS)oM5XE%U ztL)SHzQ4Y`+%Z83-XVBmFBo>rik6)h0KW{RK5wm<$H}-X^{YH)QKdDh`ci9)04ixV z0X5}D*1VKpT3EW)$a#z6x0@LnvK{exbSy$3ZjkHFlz0?_8i_x{&4c1{arJuT6ZeLQ zKA5N#{5cg%!H~6$?eg>vDDrSYPz6nzhwP?&7x}$ee1j844JP`{*V@gTy`!hZ+s$&} zw1gtx07y^;brEOY2h~?I=T7648VX&8Nk#V_G&$tLVT^zPsm_*L$UH>DyuB#&yo{he z$oV)}B$$WPjoobXLTxK>7PP^ISG3G=Euo{5Si6C)A-EhJ^){xpQRj+G4J)5QX1+Wp z0_rRebH126Jdh6$BCa#W_#Oeo)^rRhWQ&<20@q0q#XL+%6Oi-)`ce)rF>4-42$$b= z4hwVNEfjd6iHw^Pmn#!c6&b|C!hwjdAE6rs#r=nSA&PH;IS9u0hmScgK&{ts89ch* zIxx7x?qX|?j;1-P#@Ya*a;zcD%#`58hutGWb8zan+C*2Qxy}=EDQ~y!6P1Bk!K*yz z`&ZRc4klNxAXq??jE5sUWD3P|AEv!R=5wp;?pV6X&tvY?KpuFbu!CpXG{7`c6rg`y z!d5UsjE^x)O-swhwY%m_=EVx!ahYUkX+hf}Lt)7%eR4f1Z%SHL2nrjfC_3rvHA##o zG1aJH8kwxA9+#}@6-9)W()GojD#V-GPkE)r1nL`h#vi^&j~sGv?>@C{m(z9TO+-7H z6A2MVoyOigfInV!N)sC((pmpICp{;80Q`W!Wct@osK* zMwk81ngXFFR{TVI1rkMG{wMQ~LN zSh>m2tX0VR`0hDwjunPNNSTs;uj>Et&DP#Cvop2MUqtVkV21yYf}7=IT=28pq7h+f z6)~Ffg*l0?RfO}w#&g!_)IYKyZ#ypS<|clCm1`X3e>+kbHs;vMUmCj%+d{9}%3{Ml zly~q3cdrmPHCV0)_niU>`pmJwW&;)(pSSDc>T-I=x@8|iYbA#)BouUU`dS0p3n;~Z zpecO5Fm39rJ4vHPPTfPd(k;s1Zz#Rhf1`V8;r0H_+%&^2_-!S55dP2J=$R|Y{y}|9 z;7m_2e${t^CRhcCKGu0Gbvp6UrY&MlM_kTsE0UAapp95J#916{j)+L)-2U~)LOpn% z8H)Th+MyEUDsJb;;nNfBcmq9Zo@2iyB+7Ky)9XRYg`V^(r z@H#M#(L@fiwYNrG%0(9;3|QR38K8tBx*_s__-3U7OG-9wsHCZ~84LYXbW=;CTc!IQ zg1QJ;_aFRlA5L!#JNS1dV3l#$6@Le_?{_^ou~kB z=*&40O40f3_1V-9UNo&~sFQpQ$(#to4vFdx0Ss51UaPJkJ?H1!)pqbB{;xcEwg^RV z1LZU~&j7Dko~(*4mUjr{TerWMBZk5bx=rjFC5D2mUS+V*2SU>4Mv>t;r$Z4fLA3kaEi zmM4>k`Y;h2BGQWFD9h3Bu9usdp!c}GVp%J;3|VCHr^9WBnOYU;Ne|AofYSPQk~csB z{6V;o$)Gil%eMiC31C)^E#Vl0I`2NF`L?>%HFMkFj)pdvGRa2We2 zmUurd8cgt+{_X?~!Yl=d-{r%f*x}(q2HwhIt+)npTs4ysF8c5BPYGdqQP&(Q~Itgj;_ih5d2f z%=*b7rc8Z6t{N#_{Tz= zI!U1(YZts$dd~;CB+Ha6Ua(r|nz`+FZB?*)M7u)%A^4i<4qS>g_u^DxAEO(js+gSZ*w*0 z{^nQ3d{DsY`&g4V2?ZhG{!|a0;Yi4<2!g8ykx>x&j_6EnQ&&Ba+zZ(zp^-FL9nI%I zY>`NvyHIg_$PFAR`IOxs?(gS=c)Sqk1>#4MPi0&tT+Qy=3~O7{h4QM^Yl5Q@;~f0R%qs}z`Hv`^3uUEMQ4iL2P3e$t7( zK*J{?wE>cU7eY{2NEWc5wb(>+zP8k+V?Vqilu9pNhoy4R_|^4}^5Kq+Hq1$pqMRoc zN?V@fg7{!kSYsEwush$=;P&v+m@^$^f2U(}4I@B7v!63C9AbIpL~TkK+9yphRHoDcKBk) zl4y{rQ1GoXmEQ8$4<^Z`iX=a-hf>00>5k{nf*0MNDr5HHP84R)CRNGgvpt}C z(KCrcHvMJ7)3T_#AP%-!cU)kgjO}(sMFX8&UZJ&Tdsvn}BqOO%)}TrFRz+FS$+Kbq z3N_A9F8OjbkqDXm_n|AE!o>ogLA#F&LWhv`}QBJ$cAs;_wBc)zx+N&{quQ#J5`w_qAPyO5B^>zDbSYL}>vHu{XQ& zFVATgrG+Q8Q{01fUBLG&Lb*YGU35ycrlacrT-1N+sWha0{ta{EQMHDR7yAWhSiGd^ zRfO8pWEAXp5+$IRuOOjB%%0BvYGuGpsxly(+ZlPH0|GmO)HwN>p!8(EWwHRqNSAP`)K8qUIhc%=l%V`FzTzRKrt%sa$woN37_h`mfq0%U>E-wf3^L$@ z{KI}wroCi$Q%CzHGN2^76S<^RqoA%sCZG7NG`{pi`is3^du-)BsNZ^OOSPG~PzJn7 z(<~OUOL1Rz*~aeM1E++wR`<^8;JBd%@00B12`SaLPC_ii7Xe5Q+vuV3TZI_6hn*P; z&U8-dT_(oM<7FDJLHCA(azJliZ@Z2O0(=XV%|!6)F*9G3X93-OL_L4F4qRt z`Kb}a<#m`p1mlsj*FAXR^zDQ?|T*@Dw=) zoU>EA;ur7UsTb1On{!&2Q&w2^B(YxcLdR8u+Y6ayT(X-1n3jOery4QxS_p`ibKo zId9vKkMstrN?F%kIkM0<*zWI&SxQg*_069}EuZpqrcd6jlKsS`bAkC{ZS0=-{!gP= zYp8<26AS==1o^+|4U~7ZuyeF<{-4>5HEK5hyEpJh`!_NO8b)@1!e>G?Um~bHVh8i( zy5DsZbx3|5$)+hmB&PV%ROILLR$Mamq`O}WmZWsV%j5R;;9;55x}Ok6cvxx1hyiIj zQ7F9uqL0kcc2(Npe1az?1Cc32mE!^z;O7|1cv65$U1DTHl0>O!n1=l?jRRT$Ey@{n zE*36;G)LQMSdqnVH?&iNeRzng7oNDXqr*nIi8kB>y|qNSQ7 zOV3?1K}PKYH>h0!%2?y9b?K*jM7fy3k?pD)MCw=%lEx+Q(_`dz!m}_!9;U>AoOO|a z%*Envco%~+g&u%GX+E&wRdmZxWCmXQ!FaaXg zq4vnFrC6I>8z02+ErY>jQEcXN08C1>%Vc(-lF_|X{uxG;EJKjbR!h-xhu>!+N|BVR`#Le(noku^K}BUs|OUK*5w0MLex1 zwCl=$2x{l;avsE|MS=@TqOxf@XpYrt6$>RtS0v3Vt3~}8GbDy3F{CJau+cd$S+H{8 zvhBue^nkn3$utv3BGe7)$zQ1{a&sj@dOZ+m<*;F^7g-DXxll=^jt|eL7&m!`kHGDj z#E#Uzge%!+$_(6t^G+|)=TH|6YmcEgMQ3TI3WF^sTMrW?Y&V7ZnFP!G@<@q#LIHm# zleFk56m2-7CT^-@#`V=w#&^10#OWhwZX1w5?1;%!idJ3z$FJJlG z!k68HeN-Gk*9Ptl@$>mXfUE5sfiHU=u;93l=;a=q5gy#S^I&-$6paNZbO=#L4btNy z-@~ADN%uKKlvaMW;SOQOIEIYQ z$TgwnnWZXcV`&Gi394EY^xL-W0YU0gjl?DUzL{Z>f@&$WRGVev{!a~q(`xZ?P%ojy zF?m1?(;<#c``jlGyoL^P)`u4TXTTDW0s$2J%qJ+li3)UXRY36;L1`vI>AMXYT(sB2 zP_=*-oiH6XSc5o5Jy-`2AVK$@eGL8}>s0a(HUi%PWL(pRObz$5R)f#x7#L4xS!Y4? zIOulq*O}3&ZZ%kmQ|t)HfqCP}G%VYlkUvDd*-;HRG#KhvB4#jgAK_+qd<6}#Mp7mk za*3$1uD|hAvf0}Zk5WHRg^0ADgwyf4Ev#rY@ADE9?z-v3+jpjA1c27uPZj|tp2cFR z-H;Haz<<`hC#k*en(W8awc#M*h1a!SG@uNO%1DMBlNg-5S{*NiHAbvRZ!0#zhSlI; zN3GaWZZXCL-QK*ODh!Rx(k@}yw@G{jOV+~UsL7Hl;!YQ%{wzTYew8gr9AF!+Z2+WeGAt8<8ZS;~9CF;Up%@$@lEpre4D=|i= zbAXUPHI}$PfJAPc`JO^8TI6Uxo!BA2GY3-!#$!}mg2oB^YjZbLI?e3>Mk3>GWiV#& zHvG*$A#1_4DwRRb7%+fmw91h@5Pd{AW&tI}MQX^7dSDf71tyzOD*U;4u#3X5GutzN zhcaeVE?OzYh;mTNAuH5?8*^K#r_<0cmz*a^P+mltx81`L{^fVpfSNKMF>Ve|{^PcpUX~rpNrC3i@Xj32j3e zlzX7dRmVcy??>bqI_Y@vsF%iI4O6_QyYRI~9sIS@F3l^T$8n2p+TXU9jU|`ILy@xs za7P#w)@{t1qDRQg>r}f>IT=oeba1t!N=JTW-)D$*k@oUIMe9uG#D(H3pcg)r^>N&S zy|K@$_s)3rb;UcPdnENlSH`NXIpH?br2uyujiy3-D~U!lFo3C?e&jL$H=L=kTzof4A+G(jHle|XW%-2DC~40^lIGz_mu&w7Ti?qVI0vK z@xYH*E-wb-wKv}Bv5`c(&DVc&X;8Xcu3!ISU1$Gm!zKK$j!U8c+X^pb@;?tXo1}lZ z4F;HyyKfY2I()S3)f1L_npBBjNm zUes@|#!Z$iF@dT}ZUrf6+wFh;nf~7m@A}ZMz(P17#oaZsT{CNw6QzhtV zQN(4yO~{80>8#0IWL>I?Qala@Sg>lSjZv9Rr*cT91fs6mZB#Cw29|xWi(V-auo>2q(A(~7hqzObs~w)j|yBl1do&=Q^+fdtE!VxxJDGr=vl|(!te}Tb>+Lw zxdEH-!vgNJ$M;^9-1U4iWwS~nM5;L1*z(|LW$Px^T^!$Q*Jk##YUVi~ugqv^JG!|8 z%f;2HTuIa-Le17^D{}ZDEod-npP22xff!7A%KLg@sVY7l06586q)#EM6Cu4b$g7T{ z3E!%2+GcLhZqL}+pI%&DGWt)!(6U}eEW0+VFKi&6UVyc0R?*G;kFiz;$|fDbNt7Ck z(p!#jTP@=Iy9V!S2bFJ4Bx^3Z(KrQdeG68)sZci+xpJ$C12Bz}X-e$WCcn4P(KEpS zF@5wAG?hvnFiEV1bgKj^dRO|lL|JeGX*mX z70oGGjYJ8x0eiC|8N)gcHS+6GKE7|qw9uXA%@jTZ)1i69|9UE}m1I^ldkOwAR3aU( zYRKIKBz?sKXr^!gYsM6QvzY;9XF1AOP+cdtcNd1(<<)&mhu`lWYA&M>|6#F(mku=; z2AqT0mFnz$uD-;j-MzZBG1eO>tgFGR`k?psy^dB8=N8qXO@?Og5|id#@0bBk-445{ z;o-lF4Y~z-A?>d5`W9jUAHcpWP=%6}9W@=!so+XpZB5My-dZ-9uo>UpUfuOYQ1^F>p4lWKVbA*Y-AS~zHVT4eTgOyj6{=rR zY|AavQj|(hS_403?O+6-eoYr>7y6r?@R|&_UvmcYp4}PkoSk7KZ_CAI(d?CD1caX< zg_DIiMv6RUqCVB%&|&pzhEuE2C@LsMXM}9(R3{=H$Ee`*_WJWNgo>GoMjUz}L1^w{ z`^cy|JXR`p8vB_6Q$S^jLB=LSYq)7|K!obB5#AYU5tqMnkz^G+n$6*q)v>#t-CCdN z)m*&x;PPPHQ>aq^gl<+k4WUuZRVeWT`=U>MR-;s18DIjpvNLo9{Rpy%{2^=|mXt9j zsM0hBnB^#s`kX0Eo3sgx)<|Ox{BeY?60vlKGO1($=@5Eg80?53tnq;5#P|IQHJA+oJU{;Ev4oq}&a%}udjSHg#}nX@KJrHv-#n^GY`3#G+XW41F$zW0;@ED_aq?!+i3g%P5)anV0CDo}6B^y9+VmtrC#WXd z6l4IiH4ie=Hv-8@Rw9QC0(U-bsBSg@23~x1V3&{G-2LU~;T4G_6tc0t)FUzhH8WCKHK?Fg|@U`)`PwY56NnLwx!#-KP2 z9#_$K9!O$#%v4`0T(8I9>5NBiX)9}a*!Gcz%(e?&s+X>jWNr}lo%7KR(%`O!Ww-N- z8hJ2)*>XrLs2qww8!JwaB6udvHTY4Ns#pC!wRd{vkC>LdDfP=~{MxL@aiDsDqixL{A zIwr0yTuT0mNkX)YR19G+QEE&nn}@C@mdPj1ng`-ym_{d&1KnU-o-Mmq#CQ&yP3nhD zyIBVqH;5A9QVf#R6Kg)s3y`mbezolEX7ef>qDIEG4%yp>-aAG4o1MxcY4W$AC@nb^ z(Yr3^GY^4ORv-R6;s6a%06mMN7uE4VE}J49ds!& zBl5HBRInXau#ic(6IalKHv}(seQc^i%MQjMHq|f>QW~<&jmjh+(fpDxnXD~j{QE9Y z-P{f$;)c8;V7FWy`loZP8YjfSZHlstG8`}Sa?X$LqC}K-U`cqkf~ZLhNcH)&%5x9z zDU7=43mnTah86sd4qqaB9^74LP^_0XqMQ!kMx(S>Q9s_<{? zok`}wL+nAKG$;RVN523wtiSwvhe~jWK_hY*Dm~P6WUOAa85}+krl+XRTFiDYKpf?>68g zm-uHJh4D(PDL$^hk4ajIdbX0K^hRi5>zR^ss!o*MVr7M5KrT7)0{V`%a(x_Fni#h% z02W9#;n?A(Aa?4Cs&UP*LeA;_H3RVvrN?1(O?sxc2v{9tZb6!^`my+J{`*Cd znr!Td$?!Uc5Jznna+U??+3mssb{3}`3BMgZ|NtZF36nEz=A0$U10U& zDPEb>rc9oQzxFVyYeLLU%JpXoOu))UW!pD}rXKodp~YNOYWNBaoSFV)0nM_G?U|*W zy@Xq1{rNrmzKDcI9m8(+s^U)wosBn%?%D3wIc~r|GYAIqOZ|vxPCYMv@lZ@YRXk2p zys+zbddO9EgnxqK`c_o z6HQQ@g5nBH#F3t$Afc6BhB6Nm;X!etM9v~aTKhO&EW!je2XiWRL;@3=A0vDAA6V9c zRU(b;RDRC$D!>#@_q~Wgrufwi`D`Q<6q!1g&rO=YPzL^LG>q zui`Shc5BL6>>oJwN(>-HGmn3yisLa5f^N!6n*{LA65EGW$D%V0%LnX>kVw&+^vj46 zBth*$v5V+xLT#nj*pFZ}@ftgFoNv_N74Jgs~JYChzcI|Li(iLg)>5xZvE z5B&8eO9EewZH{M^u7t#-jr)hY(zprs@MTG|g7?HN7jE2a&7NnS42>4zPE4=kWICcx zR#ssd+I<`0cI^~Nk(v)C#L`FbQr7)q5(nIcxujq>Dh# zm2N=M#HU88+mV-PBB62z^geuI#p3IdNTf z#i^I!cnD`aa?Cx-V99PWxpak18E+&~%pKlv$!{_-cZE(FZ`g9o?Y~Y_Q&;$u@kT4f z+@cJZ;wIB$Y4_3?9>HsI)?V^`XObdmV ztQjbT^pH&%hhHnbDf02}M)p<%BRdzV-a|&O%+SxVM?3U>EuL`Fat2m}D=pH`H6^XA zMxd_~`*Arm6Cw|?c2BieWO=L(5xqdPA7r*sG1%rO8)!`N>c%OihXuJTTg_B2nObWYyt)3WE1#AAB3i!W#{bb^p|$h&F3 zKs1qdl74i{e3&*{G>1%FlXhO8lNT9x7<;sS{D3oi7@gCv(y#XJIP>1Sr(ezLE$V+h z3cT>(G;c-Uxb=~8mU_xG+}Y6^xzz7C?l^uY_1rn|li*IH$ha<1d#Tnkl)Am~UNq}+ zsg=$kwx#3_6*PRlQk08xxD2`7*Lc&j4#UfFnmTQh(qX^sqwNhSAhc;(7>T&vJadpp zw2fw!NmVVo`iC_C051whG6VSy`JdH070Ef2EF2I}H|hU@7ydsAo!wHmvcY9V`2u`~ zm%%j3=W7%RA&d@1vySJw`%@r@BjWZ4nCxDA>kCGZ5_y( zU0kMNXUudvPQvHC=o4@)I%c3Xf>9385@Vwo1o%ik_FD(W*;ZF@p(~#OAVwDM33gS? z_)bt7{hCG_B?$55l^y`LO4kj8LYV!p?s=&*G2><=6T1ijh<>@ zMS(@#ainCWX!DgyV#(BUy@{iS=2cVz<(u4?^f=G%rKc8zf#nG5GY@xTHjrKT;)PK` zJA+Z8iPGB25kZ2~JwoW>4zpff(NJseH~bpT^^C%ZFbytCMn)0i(p7Q#T@AYSZqLVJ z(etV0d>flBRHAs@xzLq>V=j0TPDa}?$!toQ@a z!6=ww<0dyMjbNQ?qAz0TDcgV`P@~Nw4B+>@Z;HgOBJx1vfMEypD z2-b%EeJFrQfZPOb!)j?)gT_uf4{F)cmBC@9b~!Q&JVmUu@S{jM~l+@{R=k^ZYVb!nu`sZmr6^e3TX9dWuttDX=&J)?@=@Xf7GO2teqy6&c~6>1M#()g_efx{Q>Xum`TY z@PYm%TNycG5LT4ga}jeY_7kur!O9t7SPLqH(+=6%5k=-h1uDvdcEROzRR0nc%jfBQdc9Ql(zPO0)r5ak=SU zLBtRHRON7>n$6%xgiaC~?Y|rM4MYYHphbs(g65Q`p@A8rg8Az(9<^Pi{gJ-ZsagT&69tZ1&xj;>mu$c zs>2kqnuqH*CwzJsQ*KjvTJ{-pQgHkWOC#4Q%wW9RaEnL4n^`CU_u;eR$Z zCM)AW$siwfK>=D~xKG_rsS{t1hYI0XbNL)Vu#d6JLdyHsA7lWYfNwK^q90Bz1yM5m zc&m(RiNT~s*tdP_A-?k1g@ZM>s+GLcAe12}ZIHjut>^efnhCkQda~?O$ki?$|C--X z8bAt=dFB{ZhYH}D6)g1dUn)&re;u9_yAF#_Ahs1-b1oQnwL5o>zU?N0$Zw+Evk%Lc z`1srE7}6u~0Ab-Vv=ap@8FXN>D&7lAsMsA_L@68`zHG7FcJPVhMpE0_pvpZPBX^Y5;2|DiaL9Fh^=Pb!lw`4s#rCE!?!f_jY2w6L^|RntHk z^0}Zh3$~;8OSNJL{z6L@(&UvF{K-Ucu@>@s`s7|9a^U;(^exAKMG2D5s`kT9z`M5# zQct5m599{lDBd@OQJO#GU=8_5qO$lBqq224k}TITQcI{wt?U(MJgK@qG47u^qA5B; z-g%V;VGY9k$R5ZaHp$8~mYH{*>}($Ik3+F&hLV^xa>((J%SV;^-DN}&`3Wei*z!*e0 z7%ycu2cB@Kbc)h0*jY!HwYrE64;x7|ti)b!+9>8&3*CgkS%ykNwvrKPLy5Igvd_bk z4KgpDk3hlTQAG@%Rw;l%RJqWH)Rxe|R`GZDb>+_g8{&Ss94o2qU$zX0J#tL1L(n5LGPQK&tW&) zRm_{fGBGKlXELc*Wf;D!QXs=}m$yBSo72rWB(iKm!CorEVQmj1(>ph5a!R2)cU61~=>H#B1*3V9Hj z64q}Tq>jhjYI?8uvw0V#pg2}>6%vR9a#)rjVbcPT#N)=MqrXsu(pT3kF8ZTAGOqW# z3UhOdc&l)T#5t*?*^97OS?o=$ZI&joB4eEl!pF;AxNxZG&8!UUZTPxaRbvE$%|w=) z8Kxb>fH332u;0fWipKU7n!kSAx3CT~O0IDREoiN;;t};pCWg~VIzXb;KIcYtqe{09 z#y|7_!td*tf@;Ti?m%tsPI0zL^u%W7mkX_KFudPRqEQr@D^yU;7Cv-P#Dnn%4jnmk zq-$Id564Yv|l4Pd&kTFWLXAb}?s!bI<%H3AC1 zz-*~Ow+=x5Mg=BVB@2h>K#kGcG-x&sxg_tyHgmz{(XKHP-#TzpPO30I#sZ&R-CU4? zrM+ad^*oa{c0POt9m`5x5dFIXI)5jG0w^cS{=ko3l4FYFgG_$hU7pGP*S90z(cPz` z`uc!yUfu}yJ6&hax04g|Z#b@>CZl+wM!J>Akl%>0CO1=qxV?PJF~)deGeKmwL-~eF znuVDPN01^m6@5Lmc)8hAyX?fcqlPDG!Kc>^NCUmQ67xX?m|FDez^R%7?!EW})f@5^ zzkBt1ZkrcptL@0CpjY2PPBA3mCAvS1(-wZrNQDiPsfC{KcL$sli+=<$))XH$d4eX6M89UoNKl z+E|v*iumT}Z~?u3fF5pR zTXy`G?%nUeq2H>qJU}p6Ea~^xYbdv|8F~w_?;!I2CR$z#wWv(& zvN1=E!g)!quq!d$*j~`vn!E}jzGAnhgK4*H3heQ`KN)d>KCQbtgngFPW^LjLhE{gx zy^VJ^lfe^gbSlxi{Uuzc;ZiO+nYXr?KT!y&v#9aYsB~J_@mfL>UtTGd^=#kB`;b#u z$&voG;kn8>XTJ4lH_+g6-5n*>0sK{ufq{#*wzkBL{gME8JvH1JCB0tF2@x@sE!J<9 zz&=Oh+09-ZQP}lP z9d(95SV`z9=rOfLi|PT*g)Nbd(-LY+xQe!^Ra2zcQd}zfw#b?Tqm#~U@u&_h;6gcD z+*rD;fyZ$7G-*7Ip@Fa_U@4MY=(zUea>|uh*`OjK~ZfXnm0tyJo4hQIGTn_-Uu{WXB`vFp$ z8qr$Xm@t{s{%nJr7}@_{*TI)mt!!|_P`)}p!sCgudI<-t9m%qm+sLC0Oy-R1reigZ z7Rs=3PF+)tPll*E_Iq{|l(q(R0V88xY*{35Ok0m{iwjDOY?reXT`px)5}XC&3QdiU z-7FN_!`|9fl6q(3E3cD^(d{!zxLNBE}kgu6*~nrNs`2?p)FTi zY9~R@t*RsQwr9i9(wJQ_NHD7WBQV`*H6QaYFZyG45TTJSkeD_Fs9?cv1Hk@aF06%B znAGg{-F|GgjW81OPT0uTYMJHJv}d0|4D`EP(Ghb<7%Rw!NCh18TmaZ^C#d_n2Uhd z#)e4Y_OJKYPJQ_@F9Jly6e+#F!+ylj?{+rVnBM5&l$n67kGEKpW-8>G9dni6;|tVj z%y1G@XvfCJ1MG=hVqbcr#eBW@LOJ6=Ko|?si#*nI;;uW}zN2ZG($gbMfSlQk`&-*h z6M`NDGF~m8)Y+P!QT|E6*p0p93ag_Xw>xJhT#d+!v(QOF%)`CyL$eHqTmb(2d%G^8 zZt+3rdRifCK1F)3>fZIyda?DDd}}hTc|&dEjZ5<=Hl>9|rRG=~wcc*zkfHSvrJthP z1?C?dM-He5>K{YC!6IYN%XvwIG$h7rKF1pg#R`qvH72ZvW*QoL-O%>*F4_@eZFXJE>oV4r`qj02>xzs2&%aauRnYet4oA>%KIP*=fQqua!?JX zgiuMTC`ar~yCa;q(!IbP|FWbOHF6)U$NA}&&Oi60ETgh+2e{=&v{-28a)VjzHBaUF zpt1`Pn)w_mu`J^Fw~Q6F=As?*1OgHuVQb~)zpdhO9mH9re}a4;KKhkazB{qz#{28l z`jwHYU|k@c6z!+w69M=ASiJ(yM$f$dn}SFDqfi__*($uho|B}2wG74aF_dH~oauM4_cvX% zEE2NEvW%P4B5JDz#09L0DjG%LFQnwj-$UnUdY{-7NbGj@gL2ae69@hfSgUE{E#@2y z(%>!Y0?3kZAfPEs>kWm<#u)Zh2LI3y)8!h42hp$tDBJNsYoq%;!*F3wSK={&B|`dX z=8uw%lV=)^w6XW54V@HN-3hka%q8w8sh(n8*}cH^@!&;5&mYjEfSdqy=ytWL>0#P3 zd~mo3uhKOiv1vdhQ2`oIN;KBL7Audd+41iomq(OJn-u(7pbC@y+evvw=`SB`4On!2 zSQ-}G=ay=pImhw-3fPPk)$wN{X)wZ0cPAIJ_Od}hcLaGt%wxeNb{`&OxoFb2!NVMg zQGfLT4Y6NiIA2PPbY`{QRs8|a*6MR79Q}N@WjcF2>~#sM`bWsSkz;*1aO)Bz^b~Qv zAMaK>F=wpauDUmC8oNtULk$**EjU)ZBEPhlNljECE1f+zF^q?_J1IEz)a{g94(v2M zHA`J92gc3*Nh1S&++D}spoH_yBc_kzh`_Z6g!9vjt@KR?^o$5!9M_ zcP-`E-`5anoK2>t!aV*zqAQ!k)&Q5wVEYa^J-{2y?~{AH*x;0wFMa!g z%SG?^059Y>yT+JSE88AB=s!qLA+u4RwpT>hz@$hN zhIfr4fpDYU`ne1KwG|phV{tA`)P+ki8Bm^AJEtOp__GIQaTTPOnvg=_0BZgR7q!U( zb~{9E>IBPZ*8x}dE>p4w&U`OSwT)jp?cvI=i4iUt7`nw4#RK^p*i?0)sq zd>W_~l7PTyI;~K`QyB^e)G+S;rYX#$G2EatmKIXtujjZOX=ugka!1aeZFzP;9!A>` z57S4ZIoBJp&F#)87b#L84taZ>&_m|)&Y&H=*6w4f*!WsTylsQPRBH}={$N&cH3w7# zx_5Z|u^)fv!`NQFO8!~V&c-h5-;QV;l+E*d@)cPQ0+R&w9jrq!$8ij_fLws^OW@~m z+!Im-jC#2C65wSs`65O`%2PYIH&&sRG*wwl4;p3$T@$QQRytkWYi%ESB3Nu@aRaa( zuJ?>(hZ^Xdoc6|4u>i>p7Si2-sK~o0&M->p9x| z-^Emw@{Y~EFp~F44VIlexSV;l(ywv}8h@a(Jd{wnHj6iV^^3l3eGjjWb0qhDpPn?W zjM!f)TLRPssvgG~X*S*13!G+s(2_=hL48=2AQ^;k;>dFQTutm^U1T*_Z`uE6tAZ)P92UsrO%sMG+ z7!vYBZ%XS0;PwxyA>{u28P#qT=c|gD;!|}#jOmcKm~iRZcJ6==oM4ybIAbPxDi*w; z0(@(pOyz743K)de&?YSo-kC$@7Y-8E!WmDDu?4<@1mCZj)@M$dIJW!u zT_AjAP70U!4&n^;g~&UACl(tD(>q^UEiM-Js_!*W(8pU~%b5wuhk+5uJ(t2Ji-&Qf z1an%D_oeDFq)D6;8)+{O`MFmq*FXxOw5&9{iHMi7K&7Dd783WfWZp;dAb*hLR_oLb zBvg1&i%t=h-(255xYK3C!S28aqdn2NnZ%o9E0Dq4N{mrJzM{{fo!6CJN`XXK^S^>lWPH z?1Tn6%xb`{Lx)#m^73drFHP!KYagSO1?#rp5n05qL3IOrs!Mo*Zn!>o31XwZLZAKs!XBG8GfeYa|yq7up1Z0 z^#{Yf^=8lFp?kBZ%>98O>?92?CiKRzlJ;_`;+&eu?1MHiitAdDm*p2q{FlSFJVM87 zW!n&Iu)EY~LI!4-=zfhv#F8H0PwBP+XpKE+O}fRY&9>}4W{A)agjyLf?tL?7Jg$Tr zj+*k1(4iy3<~h|x2BCfT)IwVwMG!w*RqpCcR8!#Tl*IT4_W8C~N`C+C$i@9yJj-U6 zV1YJ`U=?(P+2kQBS>TxG$;_;>>e4#9a$4Nk!}6t+p8v zslEBIX&^{Q#7+9OOM%DYq~K44_>tFl=?FR{7c#Q2eYSk@9{1=2ZJzauYp3(L#pONg z+g!Hzp?y-aU=?%-avpD`#*fS1lNzRPvfKFRY@?<7f8!nsSATAR{@g|cFhD>w|J`jQ zXk@JCWa+4AYH#DBr*CN_Z)9y~WdHwA;Uy_a#r#-|y53bJpNVOk?NQOV4Uu)XKKY_( zF8b~3Rl(S2QK;$TM3V~hoj|{zr(@$MA4O*G2*Wbn?vJP7nA-q-BwXQ%5(A92FqVCK z>J4bW!q!)l%@7dF_-D{rLnrun}7e-fC8GA4lv!%ZT~cbj%3HH@}9 zuflNC``Z+6p%BVJnu=}|&-I`gIpQQoKV!1wXa)+;A$|ZH>OX zd`+6rY-MmvJ|cdSz%wvL^p8X4%IB$Z>^y*~jlOhwCu+9z)*ZG|Se-(HXf3`;@vjPr zidW&Bd7{R#if;QIs-$XhmpQk{BsUY^R%&2`8A!t+h<6TY!(ljIws8e7#xLa?CM}wh z`=tvWzabQB%=?Zt^XAlp6;R2OnVS(+Dw7!ZaeC-p9kfSUTHN1v@w`MxVY+y>@gcH3=?+Vqz*J>&=oVS(${&) z`5Y89g;k8SzCwhnlIc?XsDyN@=JjPytqm0hrL-)48gJgVqr2@3X7(DRNeus;n?hBa zwtaFI-OZ>8f=PtONX%Aq7H@9ZtEN#fnqM*!v;XVu7|VDK{OBblvTl*xFd~6!g=;6Fk(#C$lcM49(Ct| z)D%+%$jIKMc`L#Sz)&ICBPTk`u8lw}{iK^y+T7xl3Q6D_k)l5hWTD?fW!#qapJ5(4 z9QIcei_?SCxWiBS~%)gb~g z^{pXfUamM@`(hR~0w#LvtCT&cQ6ql~^_p+p6JK^7kb}2QA>&=yPC~4+^&}DZF*!gQ z!M|vTOkNi}Ru~RIowOdXzQ3pcB{6=KAaxYLfTgVay)+I&5PmVh@oa0BTn`BFs;{Pu z5=i4lU7CNYFhvU?BH`WaWQis91S+-h%mD^8RkfESw0r*D%HWTM`p8Dic)0XfB~|Dz zxB7Ire(lwjcN6AO;W+$$xj%{AeBroBsh`b?T>w^#7X}uXOBg zeEY1q-7cTZ#3EeX&LWL&kvic0^RZ0BlGn!bMdY2k2*15tJmXAHjKo$T;89lNM^CP@ zJ+7j~zr|p)d&@-K%G@A?{nH$Ae2K0GdjL5u6G~L0Xy+7MlDjtGHk!71hdN0pWB#~E8GqXKJ@pE#lcGvb zEV;q#Q2frnfciVG*SK7c#^TGI<_?eAzAe~hKY5%|rkLEstp3j9Rv}FZEnqqSV}$e-%Xtiww*6>8Luvp3yewB{(0QT|UmXe+rgYCs3LJ zNh72IR`kZrxh@yr8R}4>ip$dC1h!i$@RVbP{3_7o8;P!Vz&HtqYvyJm?$rjX$#yGoVNkO6?V@jE@^X?@;mhB?9A3rdD`vPT)Zn!?RY?{+zTyhiR8(_YWn#_>MOv z;Yw7cs)6GPF*>jZt8ej z-rbo>cBhnLg(8{q5p@zgh`Spg6aD=X@uMKurI0SWJT5tT zcw?Rj%35PsD9v2Omv&P;nzaR=F8EV)DQ}3ZMRQ4>DEHcdVCg4~DA1MYhpF%8n-$H` zqgwDmu`Rj!p5h5c;q=P5X9Tq=Djo-`uQXVP!m$w)!X)+QrVZsrncdk9f{yC~v;%~d z%JS~|?4eV!3~!k1B;MhjIMx0+(h~rwP{fl$aGoO{#|XIvTn}YhqM8dJ?2_t+DV4>3nEAIy-a7jth^u) zm{_gT$oe5{Y-BFlqjj#V8x~cuqb$!CvVzNnw+y7IVT8K$g@y()H*&fiecwMRnkhPX z{T#B#JJG&>$2d1VVL9T21r*VA4+(<=pqP{zjjgFgLyo664HstYE#D3o=0;b^bC9hgMXK6{mlZnvm7aWQ09J}-O<4L0 zoO|PPds1LT)?BbHpivwQ!-K_hVVn#{60I6=cMT+Cz5Dt%=Z2^*8~tFDa@A|8&A7eT zir83Q4R*JhU;JpjIzJ7& z4knux*%2w2UfklEF&%~%UB`#8P_*9| z(X9A-3%%O;yV)@l4R`L6Lwzn~+Z|XP#;T4&LQKOZG3tde9WFk@*Gy4CsHjmwfc3#Yn|w&lpMpr|r;S;TxLv82?IBbt}5fe1Bu(6>$9}DxSd`@ml-8pwSUjv?Ik9QuFZYfb&O{bf?>XAiDz-T zQ<3j4|B3{-kK1%h%;LZ_Cz$jejjtUO_DV+8(xH-%siYnL%6~A;wE@s@o_~@ZZ|N`z zF~XGb4Y&y+U#{GG7R(#+BEet_>m_G-lHCRUA4Dlxr0*?Q0?F~g;uY^h$G}+iEktSf zvmqIv^H?(u831O<(Wr41ag$sq3uH3xB}LTZ{>@v z5(7CV%eENjg5>~&9Fg^)| zqN3oz-qOJc7L67Qm^MEzbqzoko$m- z9v!mD%fU`bDTen?r{- zPsEVx75*3wR3(HA%aN}`EBb+&}&o4aV@ zz&nm{o%WJy_bPxoqb5{gbkjEUjTMu^Nl%wAn!2E-eSo6W*jKN2;l&UF;0bnrvXqU7 z1IJE%go4r?KBPU0dM@24(?8#D3_}>GWt;DzYKF`^V@^faNcTK&e490}UL5g0At=jw znSOl~Ep?hUb9~C=de+_BKl>6>g2d0bmx2m5`m9+t^=N7~%W(1rlsmJH#&}CYP;#n% z=rDdn1xw)Lq74w_j#|Y_>P3qX>NS5av6k%`+9zl$6zUdq?y0j4jU=o5g;*rj=W^)L zZf`YHLh(LbFdR>hs_csyd}#8ukH1+30RxXs+mttnKviT;{*wb5 z0P`5Z;!Gso+~UXg%TLtRDSOSI>>n{6=$ROjoPk1-1n(`bHo+k=7K?ME#$rrwqsYpy~ZfZOy0jccs?t z9Gy(WAz=xnYTUYQr-37L!<><*Q)B1|z5wmsA`2^I(I_as~+E8gE1xxUF)i=n5~2o{jqD zK}t{IVoh!ym5fyPD(NgsCPt^7y`cqVIY@EX8orQvzPMtNPGZQ2ro9dELCv=>rx@LJ1Xa&~x{H;_ZRQs=~`tpi6jK!itL4 zp(Gafkf}*q7=7E(+DEO%(|S~;pYyE3tctMrGTn;A+U8gc*8If~_7~){Mw%ttmbsDl+ghaO6#H{U#A^M!;*6Vg~T~?|kc|-SQi$&Rd>vNNp zO6`5i*w&#J$tbERzXjdH~Z8&(#_w=vg z@CuCE{^A0bG9Uhd9?2VXk$~m7l^iX;a8JjD2(lHgk;EsXPTVpw9lG@twzL+Q?JZ3ptMT-8 z2QJ=x?B>eSpPj_QzOetan`yGR@Rw`=%44fSxV8jJHgzudMd=4XIupM@&7f-pq znWlGGp}i()LFbqgr}7-K@ZzA8d>tlDveZ29^O{sfxn0L!WWYf*a3B%4R8k?X#my6F z6pb$rA>QMhQ7CAnih7ZQ2HUKc2bgPdhX<10Jc!dDJa7#ampMIOfHxCR8mkzvM zgXm->Ucl>B7YUTum_{T!sro93XkPNPbMjm^tD9-M6h1hpP%f~W=dF`*?#jY!4&SoK z5H8pPZ{%H}yrvnhne>SGDq0J=V994v>W6JiBUnq1id9{?N=GYi3X>vYTy#DcJ@7fW zI_4mhT+8SJgs)w+coyLwY$0xJoX;c7>((2^Jg>1Xs0VW;&l%>}`I4;5iw-xV{M0!d zh&VCloGlh_g(4RlzV9n~($}ER*rbPDT`M(+7n)%^CC3iEqgz(^V{~gG6!4GhIL$39 zI^a^iD`Y;3Ulh4Z#@2WR=rmLM*#;xW%3OEV?gn0L`ERjcwq3qhmGMN0XYHGgl=*>f zF+w5M?0AiX{Zu)M6Fs-MSLo{c9RbpQ^FG!(Je8N9Z-p2R-JzyycXNMtk*M7r$qtTP z5nY_^Ok$5u1*Uc04RE$oXkA>M$ukYGPm$*?E8D{2*VHr5|BI%m%kh2ws81CY&X8Z4=H5Hd}rEx$17Jy0bOcKEN+1^430N z;l%UmompU|expAwnL$IJ{#>v@Q6smL}QgU0URf;K&{ikql z0Sy_6Ck6ug3IYP6{O^Rbti6q`k-eka|7e)Dsj1s{sge7VJb2d;T$2GCcAh%CAT~IdZ&xUe&b-K^jE0Pq-CQm3m$RLd- ze|tbK;V+im*AV+d`j$ylFDIAaCJ+`!ex0bW`>rH2{#!c8B-(}=*^N*Sjxvt%mvY(8 zp2(O_T)G4pfm#4r@J4qmlGvOk!3=3iZ*z$h`Zz*z@LZJSj8N50)Hsq6TMJ8DzF4B5 zpNn_6EjyUefI2k#1^9HAoVE*7v^>g6{mtrpH))mcx@a?$d|EJ@GNiHneu)xP*{|k^ z63+1OkBPA}T$YRu@1AJ>q~hN-I`_YUDP&mqt9|Kpq zuK3aI88!Pawjh49V}~ib5Wf{9yDs28ir^978Tfo${d1@CzCZ}sny~9(MNtv3b9Qpu z{`Fi#v$qg*s_l)o^GrL#$?vzQ3vg-?w}t36_C--rP}eSy3I<<9|H!OP{51*ks~BU- zfn7x(f2W+7UrDXZ5WBFoo`Q}|h(WIwjzPYxk%ty=YHMS2eIuLc=iFdlvg1d2$SU6H zhdd8%swV+B2w3^Or^BuKq;MhoX4aLGJAFbo^nC`u?LteN2g`Fet+%@U&+5PW@mhb#ifl_u?US{$^wr)AOn+$Or^$#GbAt~B7 z?7M}Re_fjBDAnWm$)xGv4g)L3h4quzZOfi{{H7`MFrVa%KJ-y1h%NYo_k9NIT66Bc zr+PLQhD6exdHkOW&{8%U9ggU(CQJjJRN2GHbY)ou`nahhnk1MgD|+{-#R5&?3%h&E zT+c4L!2KbvbPB(Y{#Z74+`7gMS}Y1LoQF;QViu(p+aJ8^c6otswXH?c!>UoJ*Xq^+ zd(hpLF0Dx=xkag}ZwKuwU=?y1(#D1c$Vl_{AOxHWU&t6VM3+y!|9^~~Q;?=Xw`R+> zZQHhOblJ9Tv&*(^+qP}jSFSE}Y5M#zH#2c2&Ro42nb&VdW>s{I9G6Z5n|2U3s5keBq$dx9;fq|)9svB8s*=Bp6_hNb z1|(Q0;_?`cRj03M@3c?$I4`J>qw3T+rHm*A}iK_Ff%VG&3QY#+^0p}xA2_m*P! z^+u)}R(0|3P|?&As*}xZCU^*Pz!!5;JxFRcOHgDw70QUgym#E2yWHi(p50=qq7w8AXyt zoB&ve+I`X+OLLBWXxA0>U2ivxqx$23AW-2zZQf1mD`(gm)`C&{HI{!9CDgbo-jBqj>KCe<&p~D#cx(LZwY3 z`W}yt$BstnuHJw47_ge2|ISIxc}NYs1%okU=X%!yt+H1KbRiWRI5ZNsbsxy4elTWB zKF!(ElDz1xh`XaC#o0h5TgbT`9%yV@q4o^_s_?JXBPSKIv~3`o2rs_`J8Br93N$*R zY(=!Oil5U8WnOaMg4w7C0xTA&q46`8 z%oghTqM1)WtX??Zzx}|mEK4uQ8KA^@AsurdwO!SsNbi8weHJv;gqa7mY$;idMyO0XH`@=(qfiCjvoV zq3uY`tm3v%Lxv62Jt7wqM`(T z@ZR?J;`o0)8*Ox7{BSN$`!McTj$|z#zP!XXvsxE?cyi`KsI7 zb%Em#xcGhD%l+VpYs%X}sIM0%XqW4UGOm%-Ep3YqlysoJO&@X_@sE4`6fDLJTWgCr z;V9LM`4Ehp_tN&(JOJ z(G1R{1JaN-XR+5*lCG6U6BJPsgk)!99k3dtwmh0t`4Kz6{NsHgIv~#bm?c@PMZMiJ zJoa|8AMZ6heRq&jC3yPwBX2=wz;CuCTxQLXepH)h2!t$DN_01fykAEi_>f(5@g!YPoz7x|B}$3LFHdk&9R$AwcLreBpfL zit)AxNUM@E)p0Ge3qQPy(APyaB@Bq^@cB`{0<6QkFTNbJ!lL=r8`56dS+cE7w97P- zNyY-|y3`>XaU?V_UYPb+MpYRdon*$v(hfHGrHA=|SniSm`nK02YT`$x4!#O&y0~}s z1F?uHy+h3SuVU^DBuz|A{QJz~+rU3%oOt^1ycsFIfWS2aUAGahyV*b6@fQdXDfned zViAgLXpm%L^Qd}7M}6*GnH#aTzyVIZUTGVi`2(6@_p7K5RPK^sBJl09dCWA`{F0pw zJw)EX_)9>CsErHAQk*9VNm?fGA(>jct1SGxv@OG67G_^G)e^*(DWoM1VrqH~ZkyW|xYr+wdH41bv4etT(h*Y7Tqf8t3!crLy&}Is4<)(g*wg zN-Dk80l)f1>T&^*v=Dr3dgB+ZGmAWjYW5GZeVy-s9%KK&)%eRfKy9=NEX&kHOq3cJ zc!;SIGS|AaA+gQKHrWMR#vFzr(vF}b`1_r9t=)GhkvUoBQa1sfE$Jd37tC_vLTtf> z!(7Y+{Lud@>eY}06iTiZR>T3aTEp}!T0OeuPdDtRXWqmr$$gYs&I0u<9E3uzo;8q7 z9s8hs)b00ta!)GI)4_{+ChR5pqUw! zNvcy@ic@O0UZ5fj$T}vxMP4Uy%1I`8KCZ^`qc8(;Utk~ktCSW(#&niH-v|Y=Y$HEZWAbaDQ$(7$F0V*m3sKesmtjrFd#DmXR__f491x-Ut;vYCkM=a80Q%Z^o$YJ ze%eq>ordvVN2Sc^3b(V*H^lLl14c!=1;CITZ0uue4cGDW7k$C<4H6>dwNESDInE2{ zHqG+(ibp!?_fZ*?(Z%O%@d`EITgX}q)TSM=%QHNw<}o#$CkFWmioNIocLsge@E^x} z?|13zA%ldwnbGcKbINz%-Q={@`Xd%>T17~PPKp(Z8%JH1ABnZlL&u^s!pQ^s3>!w>z_qX^<2 zAY_u@aP>m;QZzd%^TFy5bCGlhJs_nq|c+<)Au>8@Pf-;P6pGZ6OPc*jIvfm)vz$BRzJy6KQ;ms>& zLZ>huRo}CVPi4Hw5KY&jI#<9sv3`tsWKEWG+wJAHwZKIE98=YvhL24vQ_~gmQ(epS za*XNf)uI+gH@V1hb%K_*F^xy1t{f@XBWtI|k>$ezvk62GOOC6iGJD%p%TdV-q6#n_ zu^1VFV@FaIdMxdQ%s2c$=a-#}g*%H7gf6579%(!|`_9TzN%p`r(HrC%}o5PEtrIDyT&vsj7 zOgv!9>9%s;K*)YwgBd?udBAcX<=QPvw!sTde+sL;Rh~8lvjagP#9)w#<)EIJr01%X zh}tdg(lEMIEG;L9@IdZER;^4^^UVdpkZPPur%9v~aCab(M)iZNcpkB{rEoeq+X(zX zM42WbRakR_si?VuT{Wxih@)`gCMVbGcXS^G2e%)&h@^q)ni?NvgiMvMw zuT{u%hx0`h@=3)6R}~_U{A7X*Y_4aWj}&r1U-xuc1cG#9X++MeD>ipy-U0fA3Q+@Y zSH+r-W2_$djkPEVfxp|mC7?4N9K8*_Y9h^zH5-{*2P&@2-Ih22iZ_FFX@YO9eGaRt zquiHMF|t7HJN!q%5cmw4DKHg<+4fI&EF3U?_T@3$n`KQo!oaILbs(RVj z?0z+wy^P9+)L1DZn(%}TYa2H6^X3TXPIVh*GHxQ2UepUyNivRnaAu+05di^8L{3~v z1Cr~XA534(tU%+(Ei7_n0=pRh7FYlDyuBg=erE92A1);DH4d;R_gw7GTD0C6uT>l} zez|Dg_4W*FkY*fBF2eHnj+r}H`ull0dpNh0ZgS8Myx%DWrN_e@j0j$M&os1mi}c6aN?PhOf7HR+tm!Xlp!6t-^=pnzJ+(s%=)%(qPo?)EaV>i9 z)DMLLt{zOa-T8(?4)cF%%<%*Opasw&KB}Eo+_Xxv!WZAIVxg9X)BJF6TRaH5<0$AW zXb|)b7=3ct8U<6uT2WgkMHK8A-;c7ODel*8K%Q~Hi`Yd{ z9FqMc+Tg*FZ0KOwO*8__T&m%l#esU*)7)vC4t{TQ0V(QM&(<@kGOjYN91HoDQDbtN zK+%_Xz0`Zg{_)>vaORdW*p^C{50++|>`ia?qPjwrpg+uiS5 z%IBw}(FmuGzx3-tCf#~5PgH^g$7NmA;LsQats9$`I(pq7Z8L9E7Df*sI#EF>@?xkt zQoI3WWWwK0`{VQFBlPO@Mpd@UL;5>@R6tsE6^Uj_vDRVQ`ET2w=Jl&c5q-00KOpmX z95u@*2XRn#SsG=nJTdJWj5%~v1aoug5kb${=_9Io|A*xc`?K@B^}1d&WssbNs`w|i z!*obz6!f^3yVsX!!I%I<#D@~&xJIBXs=DhoY+a0M2CDHE-a=Q!h>=kW@|#;OA0)3H zkE?OZWEXsGyY`RmSDr0gR%DB(H0yL>E(Y4ux!hjHd&v%U<2_w7^`}LFipHH^Pv%DS zT@`%HVH`Ub-dt4qOr2pHjI~$*KrJ7Qu_I$8liCR6(%9OBq)(#C0qry*UOJQaRQ0ucl2xlov#X752NBd5)Eb+ zaM^JtUR~O$e;b33XWkhv4$U(iPg>zvd^BS|(V;&V%Z%E+9|cYpX^EUoQj}}%^HGvo z?r5}YzS!LFiRic_o9rlV`v%o?K7JxjcM+CwYpSc+(0(h|n;r>#q%#RCF`;n)cm3TAvj0qU6PG45(wn zp&^e_kw760sUK_v}4FsK2LfkRc(~SmIl2_M0X&<3!No77f{fsco5C-RDW5-H`e4cl3^_g}CCCcDTVJ*gItC!bE0m z9hr5H`wPB7Y}brkSz;WB#hF{2N$<>yXyNj`XNB8cBAB}7`Wvv&e#AI=Nc4{Xj{CQr zI)d~YK{_X*GCOUUZ%A|-GMkgak07~glN9&$lkvmI8=?hBZQi1}-xqFuB2SsFEQZ`3 zw8n(quTc^>Q+^UHSKawsa$lx)M{;%1G;~TM#RQ}9um6|+B|j$YlZUO?&N{pduj?1l zzr;Y?B3+jI)S4lhK=zN!D09{v&P4?p9xM>|NkX4%P+x(F2eisJ0Y+D*&dv_g_s&=> zPh8-5;mg~IbUqq1BNXooF23hsCqVwo*;PXVT;0HwO$U_I@A?bwxBP|Y5%9f6dUueL zWw$37?6&nt`=H|ukYpF!gfDX7p#o?%O=puAEUkgk0>dP5QLHhiro}{c1~?NSJJ>hw z%eahuDeqf_@TqAe(yabdT>pX(pG)a#33)3`t*;UYPZV0vnoccdPS)VpDuW-&Ly?IF z!NCSy2Wguup8aA z>6|AF<7L*Ls)y>buT#eQ?KAFqn?onmj;Dxh+Bn;EYsm{OyzviO=wynxI3r_StSQ9y zYi7)?9&If{63=~j&5KlXf#6tgnMny=;2iD8YIk=*&nf+?!+=6qB671Xmwx5b`|ue2 ze;@}LxseJNQw!l(qi*rar>A>V!h>m<%b2RLV=|5uCKi?)s|Qak|B8F$MLT1&Le1S+ zWU48bc{-1y9AAI+7o7U6{L+rg?)G{#QAngcdd0FH5P$~= zw&St^^)O3cPb)iYdEDTm1K6+BeTYp4;&{&okAkwxk|$u^Sco1NLUfT0&U=P}yH^&y z@Ve`%ov5W!8d^m9`&*@jklrbXQ|_q9fI%qdPVQ-S)LX3hFRqr6g&cR ze&U@CQ3gqLI@H~%0pdGmf)CN|>f{b66nc?}`=amZQr5-iTezQ@Cdii^nfiI`s zk3;i>76Fw*93J(XzsuxaEMY^!y1k#qt6?qm1du=*5m>+BDF_3PENUvTMGltLn`@$t z1ofhD$+781K5w@lo+)Cf-@CgHV_@@&P+UICBNt6As2JYsH`}ru9&x&}yJnCRmK>eh z!I}TiRW{J!wzu?n-0AY<9} zVp_o}h^r9x1NL3_oxDpMs@n9h%AOPXWiZ@3N!=*5cl(Rnfk2Vw|Gu{AZH0aBsbn^h zZ_B$JgP|j!II$4=YqHTW#{+m!oV?ux2_C1xRiZ7M6l=!MrW^LxnQn#2sdk%#Wnxzb zgg-hBMT2~MMZ3W@Q)9Cu&_vVlGni?rB(8u%4}^TL=B;atW$v#RrpBjt?Lv(3W|P0A zpB}!2@DNN@>GC0FJef)os){*}h2PZPgc190bWWl0*wdZ!GXu&%z!q3Ke`{1@95Zfd z6kYYfWjz$AgCUL5aQhQNaLATxI#U&7I> z>Z)Z2kFeT;{ij3c+cME;iKr$ZH6k$@7&%dLlbw&K$FV}3?%!<5p|OY8zRVrTyU+Zl zvA2A|POgAta3U1erN-GDVwoMTLZS{R4-z^cP0Sw`To_lC)roX-L`tKjz(TNY1g_Ox?PIq4d>&(w)w&(=e_4;?OI7@==(Y@Rg27O#Mzh}qVaa_Xgm)Old z&{o<&Iy4nHUuMaRKoaILXOT64KS8PRp0eH7trv2PXNcoq?b$aBIdSyuBoJir^`n_c z1FHEfKGrl2Vgs9=9FZxQw(9KdnI#V;85!HA4liCX$Dw1CA%FHGL^ZWQWk^O*%5)*2 z{q1;h(a`&9sPDahIrLed0xodjJJLwHQSTXtPMK1SJX{2efJ)WB+2dw2*NsveH*TAP zd%Q7~2&R2*GHusuOG2&U)S4r5=j@#7%+29{tp74rFLYn|*VQf-(8Z(wU8b(J5;5>j zU;#0js62HHFWx3F%{`fbo=~6Jy;zvjGcJZ#xlUctz~{b>>Rb#EgU0Du;wq%{#hV7Tw zimmVj%fC!PWa!nwN4ApobRCokdf1&GCQCh2TOQDZAyt>_Br4jXwX3hIp82Qa)e$f} zVv7u68U|;1vO77|;V>ifUlvIlTPgt}eS|J@d z{!(PPGKVsMY&oNYy8vpzhp9{$CX7Ur?$z)zE>n7H(}&j=sa5U)^Ag$Xia}98->WJo zUzs;9vrjHQBTc7X_>$ce2VZ|-YRtze=-hpjd?Apy!=I!;H{d^`QFa)KG6(Y^=z*}Z zO!t+fpdiD6Fv=xigB4J6H&?JV&Q#Kyydsy4F&H;t`SC$>#ST!F{lL~tTdL4=Ao(`S%PRThtps+(yVuOFRn$oR|_{C}7-t;LMUG#Pu zDO%3^OS)yo?(PtkdFrLAxHkAO+vYi_#wX4)$3w3R@+Dtb2M=gt^rcmRP`P{`HJ(GG-u1I&f6I_0C~!=T&i`P(Dt8(mkXz>tbBB?3ELe4XZ|^ z#W*3EU&l4T(JngTCd0oS%{Gs7=k*F%Ko4BePCQ^$tNfFe*IN5u5@o@no4OAX8KxzT!h=b4BEgWHM7x)!2pEe-NsZs&Pj z_IbrhFN$%jn8kta#!c$kbSb z#Zq1~2^FKr^cInVQX8-e;6kn$D8O5S5f9)9#ap+{*2z&$8|T)@*Ax{2+ZJ6IWfKVF z`jj?Z)?FG|+J}WLijXfq83qM+Hq*p4atB@CO5&}bCf@2$f&n`v+I-?H4w`kmOw z$2+k4l3*R;qEj_-*;wu%8vS8ib}bf43bChsao`@fPg@Aj zf!6wESxU!`ebSk=_Awj{HC+?w^BHY5@r--=PeHL~0$sA#P4>S23~c*{`4BAAN@0PN z1KWv)few^+6R&YpoNM=Wf8(laTo7ofn|{=)TP=lKJdOoD=`Ehef_YWy^$FTlV?_id zj~866mZoQ1WbR!+KQo7{e_rB#FM*SZIA%B{kgyN&Sf`izL09_8T@*`zP);PticzX{ z`#1agCKf^C6OJvcg(QcBRpZi@O(-uQz$#MMU@UP_`WFc22TSJlTDFnL_%G6>ZyXfz z`s-Ls^C>7W2rEH3TWIwQ;nDN4>=K{e4z)(A&|+xm6}NZdV$S{s+QA0mwaDjj* z{=2FtVMoXR^ysSEnAw`SI@te@ET&Yg00+``m#09Unc5=iAucb@SyW?{H% zgDXE?bWwpjHU0Ifv#2wDbyUBPMfk~gieFUXNK+cGFYA-}St52m!UlDBYU4zlREGLY zS+G0|!Ek-))&~E2@NNZ*Vn-^CMe*e0S?FNC2x}^A8QV5B{en!5bM+H^+(QLj%ik0NO7dYhOZ$`rs84-oy<^Bsu_6IBF9s>aUBLoISX(*h=vNWCgcC5?N!H z$KtX8ccVIwLmDbnHG$cnItSqaLF(AFfn-6jVYUwuVYNKXN!9G!73CMLKXWqL!A#eW z^qSuNsHAlRNMd#!h7iQ4r`T7}kBO1y5~DnPyck)Gfccs>*hjB6v29Azzc{d%!h=RB z(wr<=q@i<6dm1CZ zdfV*J&`sSSA=Y2M#9sv4X?7k z3rd6gTq_J}61*fWFgODv(}Cia=p6M&u!>W3C~!V0r~*ZsduCWByga(inOpXKK^bp1 z8;h`Wb=j8N61RTc=^6I?&?*!jN-hl1mEECieqjXZ#$TFD=py)KSZo$zTC8jRqVYt@ zl1pYV*m&t+&JfmOT9ezIL7Wq!HEac`z4w6e8DG(^loLAlkR?*X&BK-q3m;W=F9mN8 z2BCH_agvbu-pug-khw~my8xpJc_*}H#d7&PzMKaDn-=XQg{G+p+0~H0px~^=YUBIh ze&;iC{p- z0y2XjqVQ0!Aig%$d8*YOzi~-y$9#iNlBF_Aj}3Dsdxk4qVw(rY08a zD_n~E{td6}#14?IYY5iZJHK12fF~0`$REldN*;bR3O%@7 z{0M?zIp0j>mp*rgu4iRRcy1pGPeE}(g5mxL9nFn#6OKt6I9?vsAou%SoWK(}1a+H~ zuEr@A7{8UA-9foR)7v?&dm8S>^uk_|$%6XCW^a_TW{d>{XF-@8&sda3hW6sZ-^fwT zK=IGuAM5wHpZBx(`-=Ff>d(xyr#m=6$%hw zfhQ^}T#@dST%FujPxX04Ke0#02w5vzz)9Lls1);ztr!8X&+ERn?BYO?op0t5 z#@2(Q#r0uSq_=t-@Ljxk?OsOgcs?eb_Ea+|DMH}v-Jzx28eD_Rf#*75)>K6GE1%vXeX z58)$&_a50j(l5-qt_Xiv6Wb{%lzfpwRMaYZo{C}$D0wWD@1V2lof+~vdrCduFYS&7 zJL*5NM-uAmXoXY+x?D)uUl#FLFMH3s{X;OtxlUc`(&D!&^E(Bf*bDc`?X@= zUJI>>X*c({7(QU%yL3c*@kbsi(e3<+Q|4XFkbz=tu_R{SwHUN|MV4L5EXFk<u;nfUy6Mh1a{=~^2%xq-X306yLZpbnCj@Z0&LfO@O&;)ya2 zPAteL_`RfJzpK)1BUjR36%D=g+uo7O<)G=$IjzN*ylH@u zW2e84+uHQQU%Q6j-5%&YrGYA3bB><8@6U&?`&f93r|i*%T?$%v-I;?$K!nATlWEC% zaz|#?7~kCl-o}m9eZc=&L7&3L>&x?x{__6sJo(?#JdL~@++6?16J)D~@_&&S{9o(2 z-i#chQdB)m~7_AdapVC{gj1jjyEAU?m9*BphP}PrGtN%GThxN@Puu8`DV2wHS z7yOY~fSYd|Ko8%E3=UPj4Zc_iIOUTg2e_Hruso>aG06ZdxnCQPXV)Pb7kHO{6zSCB&B+RzntY5nfsmsg+G(iJ@Ui13}kxStQ2WQ_(%NHdL% zBtIE#OaQp4sw^I89kS6hqG)F+UU5~90V!lMSg>YDg}G7SabH6o%rQ#YktURx^2^4A z#6g50d*%mCT*fP)yB`1LcJJfhB`r4HvKAB46QgW1j?r;IeXiD`{4g5O`% z2bRI_x#C5~&`9hhenzYsJVI2?5;H`aUUWJIDN>wcbxGVNJ%r_}7rm8EHjj2k{`oDc zcefHYb@XZ|guT>OwWlPX6Rsu3x5nSbj7JV`0O{(^7hd`R2)`HRCugTXpfv(V44J|~ z*}EeyF4|ZyF}dxGrV*M-hhZ^xR40Ll(QY+uOX}Whi(=$CZ?#uC$vEaO?E%7;b zN=LhJ*n%oPbh5Nw{dXsQfvixB*+hY=*P(=QJDf(wN4OMc1v<-xSxm%Fs(AOv<#HpI z+L87#6cgMkIhs@MhY9A$qkhKP`Dglc7d9nm51Q^PnyT>=N3LuKZIvQ|CUqpO;1144 z9&W=D{f+3aOO3-n491gGJ|neb1=mu>Rm|%A34H<;lIri9W)~IgQ*E*<}mn^gdxFTGEO1eV6KY* z$3A~zG*;qfK&TC=mkuEazO#eVdW3Fejh9K_iB^!&FWHx2b+%q|2a@S7n)7kG&qU+N z0{KSo@wzsYQlq|6ElHGCkM--z=M#NCUsAsJifs#`6mJ1SPncyG7oKlIcbfN)dGUy~ z)^iUz%T5EH$scNcNH+OHC>Fz}JhkvK|Acs0ZCmpI34D>G7baoYq` zGORC3R&NXlkv(PQjW1HLC^E4r#OXyvEy{Qru<@2dC5q(aWGvJyG$IlNN_GA2ZV%6P z(Xp#>$VpdGL>4|&%v9Az%BsZE!rxP4g;QB@DmPk-kkunVS&bs4DWkbn{y;zT8f0FX zPDM7ZR9eZedO@!86;OWJtYgStoc0Fi=fiC~ksdUnMsmD!UJUBb@s?Lqh1ntGHb+Qe znu0H}nu)uHAa54|uRc3Tox{n2cbmJ&t3MONd!GFPet0{fVup|ldVzTj#X2@*En@m7 zG5yY9;rQ8}oOJF4Q0<(=8!0I0A}ZPw*Y$p{(i(Bd_${zoYa!MCY*j%s6VybqKsJX@ z0mZJSkdsU(A6*>Dk?f2^FydLbfS-SBf>EC(YYljZY5EguPvM+)( zEQ@|9Aw{U0CSM0Lh3RWQbFGM9CGIFwv^5i%9QmcBDp@pB$*oF1vI+t!Ss|fnL3141 zwDvU=uYf=_W1x%(wiVZDF6aO0?BoapflZ;qyWDtYeZH0TuF7{ywi$q1)0gCE@LW|#k%SzWoZ$##v1cxqcbp1309 zTVzGg88Hw`25L5!{m{8;9?%tA&pxv9>vE!gOlS)@SOu0`$Epmy@N)fJ{K5c6fdGHxav;L^knuk0G=3zd z)`chz96V-v>j7qNQU^Y-xGPi+@TBj1(OBtmLlmsfBkDzx!p>9GXY;fe(Hs$8DQ$bSXzgt(;Af~ygYl9U za^;S1_*}UivY%%LqyAbARycU%r*RiTh-=>|iv_)zls=(PAF@Kp4;d_p(H1p}U*=QA z9%6GBW6z*q6*$b@#8EYf^7Pbfy-&E-IdD07vcR%O9apE6i(k(x=F$6`p6WpIsfer9 zX4Mf89shc3*fm<^R+bk{jPq+oKAN00w);G)tj=j$xdF2`*CcxUy3uMz7ar!B%skq3 zpgdaHKD#i=6!9!dY@9M9QJhS4O7#5#iHrtu(#H@)SjsKqAzlW40N18V~Z+F9O zclr>Vf<1AWI9d}>y_f_hcrEaG)Yxiy^Hp^>b?AF&z|&9ZL#uDXmg17wtzrOtGB~Ta z5INiNhgG}3j8Ds{Gs@Dv9Q8=)6iJx!!QTLhLSPsv_tWkTJjX=B2VHZH7!)gR!Nyux z$VpOJZxpSFx`}R{CNw=g7ldI}hk+2Mp9MUuBgh(4Q27LvaMeD8_INLm!3qg{I}g8( z5@t)IThO5)HCDTpke`s{O0*GQcRe&E7ps5Io2-NE)%6|*H@2l|5dyaX2^N9+pn&*~ z8xesYY!2+f*s@gYRH_L1{pRPMXj37Yg3O%zsBS0x)~Bm`10Hb+LHDBpU&XzkVy)op zz3w(<)}sL$VI4;Eqw#q|_w)noy{d%7#KnN8XHod$y!R}eDyW0rt$uy&`dRyWT^j=v z(8?|5;STInS_Adn3b;C`g3jpNfjiP9xa3PhYKZ8ZFU;nTXGri?WV|yP23MC;zMNWT>P^gHGO5MZ`si1-ul|$OM^hHd5}Yos~(+!D9fd)c|x8hKft1=tZ%#tMyI-VN}eGP z#HwzkFR}?nwXA;rpH;IU{oqY7rscH@@+^5FR=do;gnCgntPyXuD;H2f)lz>#2EZ>P zYv(@BGJS3M*(S7WT0?#RKi-r5G*^GjTs+df{mfeC>P-z!Z{(bhr~F?TrS%%-AFcz% zXJW`xsAK-1jCUPp)G0AK)$qns$2_A%Cm`koaJXA!`|=2FjIGkKeTIXdH0My`3kO#(#QL5Ov~??2z>{V@p= zZ3nUDy*Y^^EeAf<{k8u+zxSVckB-^6(3ZV~TgUF~9fyNmU9xNC{`nZ-?!6atO_7nc zf+_0T07PHvk`4p1b@!ex`N`+$`YA@~U&5*VrXY7-xH&y_t&O_--8=ocM{79WS0e^s&U17_J!_u z<%UJ=RbgQ48~AyeAmA*?7JJ&Z{iW}CNlf+o*BgaP(!dm+c$wW!?g@7Ip1vyh={?Fa zkFCYQTEN0pE@u;p2Xg0HYIQ-IX=a7ROD4^J*ysenPv-d z`lQMdDOltIR|#s#YP31l9lIsmV##tg49jm4Skmm$tw)s=sW-s*`P` z>+YXYoYUusZr^X522JKC*4*6&FeO%fKKfw76%%%_Z;rPf-%i0?y>qo*uasgOz?ru3i-1{1>RTw5Rl2 zmB*)4n`|58b~R|%U><$;VTIE-jkdYO_vW=?)hsy3V~5IEc_L0m zrsJg&CO&6IAd!8S8IegYvv-}v;<8=m#1fJ(3=tu&KV9;hgOUG|RTiz`H|I1{mTcq3*{f{Qurj0 zm-l{3I@zXPzP*Ao$|G)eGh(B&XaE>eyUgsLgps_m!z%H|pH)<*%(VOR7B~VnL5d`T z^=Sq-V&OO>-AP3sTN+P#9ECxG+R2V=!b%+ZWy(Wnpx>M-2d*P?j1lx4*J5L8y1vv9 zxAbZYUi0Qmt<0vFIb%fR5^b62*qh=7;v6GMpWJF@oeXQY))bjAi^?c|CoC)N7*f=O5`6vu@S!V}y`BWGz+BZ8GAaDMuXD0^$8~mR-lhG0%i8U`c?__MV?;> z>uvrNEc(?cxG%z`)#~M-_C1p9gNdn%s;M~vT^B&QrSu;ogq~#TYsjzM+2F3Y2o z$lFf(DemU22b;jrR#y3>PRSACv!Q8xsRm5`3}%>Dm~fp5)P2hYpHerlF_i?P zzb`ceSXL5A6H*iF`k58#Y-lDO>V>r4yF;O`R}#6<3Q@icETfR-K<(j1gztX@K(2NN05n zqT~zwE|wMm0c*7)BuYwv*c*VVoI%_j2_+6SKtFHE$c{01&!(R3hve9t?ghO^`}EoU zDbmH(un;*eau;|CAW9>u^(aJx5g8Nt3|U>*wWvl(NoaEo!Tb{5zHUt$Byly3cMRCf$zN=)5aTnp8YIC@s&PlLELba5XfhzGe7s>+l|IzOhJ6v))_`K#s~09)GcK?8bZJSAIIsqP9>f95(fk5CW?~lBwhwXQ$7x%Q*ZSm^>c~ zE~E^3TE7a|p)p#`Fo0apNa0{c4qH+MXhfND>mp4x5lKzE1pI?0zOzu0EbmQSP%0#c zsqbf#HJkS}el^;w} zp_z0e_A%P;(t}RJiRtwOE1&nu*N=%4;>YGx)Fu7rk!$|>(nEZbM;(71oBPHIBy$J{ z-jH{8R~aqfCi2?)pOxXQPaR07nW=fqjy~{z+6YnK{+nVPp6?D@R>CLE==o+HC}%FB53Gayqq0T$@(<3 zXc83_;hFe~)Fk7X)8{_#{7jA%o)uIZSHoG2AGZe-!g;&rkX`JcHKDXb%2Tm+^$|mq zZj;m*S0r-{>=w_#vdZsWo`5Z^e0hto#Ld*xt+S=QS`u*;&eB!iTnS=+J8$GrF|#G+ z5xn@4f1^pD3jzEa$XC?vBrWw_H-jayZC3ClzTbxebP)db^XO^4(oR8Wp?4y2u@TvJ z4&<2|5kqd~+?f}*adJUD*`RXuj63hTr+kxz6B}Lo0oJfw!t5-^SjuEo^A9YHjRn8! z>!Vu0+C+;5{hc+RLJFNa_T*MMel5BhyIQpq86`6_+*}toMYzRe>feKsORU1|N&-`T z&kcndU-5UfTm!OuvaW5(Go4on*D?R22>EYzgp!TyX`hM; zM@;XRL>e}-Hwo{Z#LZm6ECx8CbmYF#Bhm9lB(*j+##N0!)|3nKpUMu;%OCLnEAI^{ z!u8C70RW=@S)Bf(`t)B-PR78-;Qys4H?3jih|GcZ73CYeqQ!FPXE7^%O%9AghHxt@ zUdE1@vawNcWV4QTZsShkj5B!Dd()oqZqS(>w7y}Y1H?zN{c@=Nv}Yg6V@l45){X?# z7_r{2KL{3k$EvE-d8Aoqq;dq+s(4(*hJ%0e+Y2?LT$`a-qoj#|s(zw6C2`=ObxFzC zt0Z{re~y^cA;rEQuMC~+hDsPwY2rulfnSQ79Epri7wU=^)U2Tuxg|*IPbd>Pe8##6 zG^g5Rjd?+Khb1T1w-}qJf>|Bg!@{P(EEsv|On&Fo976N(RS=cU&#_|^%=&=f9^6Yk zBFA6M=h&!U|GjQRl{NDkb}-Q}5Dsi=3m+a3R31yFRUCg%XJ)Q+>%+qbL1L-L`DkAZ zDz|XNK1_jM;Cso?>U3kpph=w!<4PwkHVaf7?uUZ)IYAD~yeEB|dmDel51?Uz&6$0l3IY%?%7`tyP!|V1Ps7#W%xbu@n-q4zC3%Zn+2@`=RGjZ zmjDT)94{cU|4Um~j^b|^+>;p`cC0)iPn(PCMYblb1Cy;(MiVqsL^z8DnTD9IY`V}9 z$GGW%R47miBBX~znAXwcQDN%)(Y1TdPgH!pB591q$f?knK6h%EA0@0<^^9Nlkv95b zTP+bILUq=+{edu?gkJu&Vst90LBG8HAv?Rnr@h=Rz7GcC?CyZR@36Zb;N_U-@H)qP z1Q71myUv*X%=o9Cm_L3S|6Yl`ag$^bAyMaXD2K6O?`o;9V~mcuCqmBncsi+2p2vvK zo#iF4#rl;Jc~fsX**vpXPu-q6B6jnjPI^EJ>NDyXl;*>0$5+RG4C#JUh^7kTPP?|M zRz6jm*7`_0AJ<;5tgCIQU#S`A70?hXJ{r7I*0rBk`GBq45xo}LK88;KdCx@}N}5BT zW&-T&dmKZ?+jYyuNcnYe`5;)SP7>_W?5|2O8P}nlqQg@TuQ9IF?tby+*XyJrl7^uI z#v%gF!I7O~_1k+sx;!Y_jI$}n zQ9LdlE&{zA9dCXcUhIx`4nldK4<}wWPCa_nr4qO`BBLtw;xg4;)YC$ z%L^{*vtE64$|}k>LPMAu!P>EG-2SGk~*QWE2 z>`<6yT7H&la?3-N3Lw7MxM5|-(D7z}xj6aoa&i-qB1+;uSJ&pb5g*SIM5TliEMs&0 zGUm))MBamJZbyOt%4I{BW8=Us{IRO?DqBhoRtrRC)I_Nz{6B0hj8180!|qh{LDOhmht(yv~%XS>aS3?vc}S0MC)t)t6Q|Q%@oRj(H4A zXYEVP03%KU_3q&ia!Vbzs8yW}MgKTNSjoOeNfZJ_j6o>Wh)F;9!&dmSv-v<;B>PJF~W`(ymH|TwZ+_{zDnbaTaWB}a-Hcu=W6uKl!{Pj213L;+T)&+|I z2KNQ`eTI9OG&nCp{d#97`XF~~Uw?MZn6rMD=XvvD2b|(AqyHJHB3fLnKzs!xcfAm~ zSU~<}Ur@JdUi3PhxHwelyUMTkuI94rwQm~hJv4=ob*wW-CX03D%Be!#&?Ph+kQbeZ zn*z~$e>obPRO974yYqk)5?xtO6L)llLZ}jClZk`=9{(+lX$qk; zv=!>p4onBzhnz~fd-=2R7yZZlj$U8bnS6d4G>di0vWElIJttSluOc=IU0 zhb9hiCaI%ss~a3eRr7sn8`I-XdGPj;Pn>~mgS*nK6~9c9GQ+SxoRUU zl%Wu`0TGKXHs=l!l0#oZDh6vOy)}0b`F3`4avoT!$ zGP$@)-`ls=@xV&7+ra@7`t$=MSkEaw0ez2sTUGlum~icwQvH#Z>1=*fOaeJ!-6~QP zg&!z}M+9O&1B;H&ke!ev^kq7#`pFgpsUGE-@!{qe$;n7N}&a@!1 zbPVVUB+&P?PY_oU?W?37FHeIk83#hGNOkyI_HI%n@{QN@GWDOohlY*(Zl#pkolupgy_i11k?I#mIf!GM3#S|eBPZdT zl< zc!D1$9Z^%sv=8#3s(Z-FuudqyOO{m1AUZ;Z%HvoeqGWIdFPcNvRqSh|JJ9Ysg`P9_FACAYa9J9*0~ zp=efijt>E2y%{rb)`I64i%%|vHaM@I4_n)H`Cx^YrFiD7Y0TSADr22M$sb!MO3-Mz z6yN()o(51&RZO_i=}QrSsp%u;0ooY28P%vwK(-##ssc^rL%M1P z5(66St5r3C5?2iz+k~j8;;tU~{r3uUX$A_lW~*@i4zy}EmX+I8y3B_l?&e*;D-~QI zDID!>d{sciSiLlJAfCQ73dM~+;iY#iyTgW}g8&dYY_zUB>p&%OPx4GmP6Cn2nUwnYJJlhF%BO>S2Bx-;Sni!Z!-^(BO zjmpSP>6l1ooZ>+jf;f@1zV_E!{Bo&~ITZxWh*r`CDSBk3(PCggW{E;5j+m9GRKv%4 z2ty>1UMnx>-}aQH2gJh(rreRPcoC*yfzmb7BHL&F4uSHI6F}NqVTLpKL;NOADF5ey zWk?_BIVXZCWg>p$R?y&uQ9|gCic8o}A0#zvYoeZe(?Cy&!WvFN;yN(%3+%*k`+&5B zIeok`xBZZ((RZdnCz=;UX|M2CMd*=)%Y$|swm_K}Qe&Bz;PJ!HrA6dn{dFQ$Abcj_ zoe>9)e&G9_iJ925xtTe8nnRMslP&Xt6Yh!Lw^q4-UNemS<10y&eA-vL8_57d7mF@f z;+IB0>Ssf8N+FbhJdTf;Zq|2e+(GP634ko2D~qs&m&)tm1!5;hp1(vGeybzreGWYx z#09-ieV9jB%@sYM|JvOAUOxMDqNuSQ-h00|@^bR=2J4*>{<&y;J^#Eg7Ngk1x+=t5 zfTaX53mug~i+9Op;y{;CzjDbX2I>d&3mf2I zDVBSs(`}jC8i8PVhx+*~$6Ute_&jXxkAUkJF*PzD99x{}oSmb%@N`3b@o957mJsn% zzt|N0_B)B5a!9V`@y_#9jawyk?6Y58Q5~3eUCIhH| zMmc8kv~eyM>QTi!zKp)lap{=!JDZG&dA#rbCg7HYnNv)dcToiN-#BFk1QcqHoJz@j z)$gFgP7@8-PJR}We9kU$JPRj8+D|yP%{>XP5GK>|*6Ro*PADE!%8$P8Ar(O(Z7cl8 zIG5t}5NQh%fgr2{ocGruqe(>63pg2SJdlu|ui@AML8%1b}aW2 z1qA>xm;o@w)Tc+Z;}8Ly9Ft{4xcFj%QtoGN{>x$;>eF(;9R;uOM>ILpShD^SbaFTw zZMs_Frn-wO{3+&nzzE_6#Yx5K8Uc-%2@zc*URdp%&Vet z7(PN-$j{%F0XBbSijfoqh*#teFN(B=9*I^Y1-%dV=p^Y84V-ui8<+{CFO5=vEn;b? z&J#TVIdC^(OJAA*;=o0K%s?Je;l~k{7NeWgCq=PGzJ#<@akq~cCpy+Mv(r}bu2tqC z*42<{N7_To(`ZuBPPSf<-IPpbdn54!aSPK_#1LNis4cXFS+Hdb%1|;QP@n%h#F0{z zPHFVjM4yA25w7OO`)xCm$`K~Er3E1z>2?`rsV&XsiB911cTV@k@(95k`SP4UF6V~h z$J96iUD%;G>|QEbiT}j-`Ka7(Wbbg?G=OVD#}1O{J6Rozv+R!nhfi@d6-SB!yEtjQOL2|LDu!oMRJ_cn8*f}N&qQZRIEa=c(fsD;?|o0Bi1Q8vCEa!h?5l>T(JR~(1u(=5o=)~ej6Vmj z12`E3cRCraZ^JOmCCS5Tx(NEHIH{XEN3GIK4~dYgE=qygbo<6vKSu!Yhk+v{$29JW z@jU|&?9g7A0e+qFge*oMhMPla6e_s=D-LQc2!&uq03&C=WrP zM1)(E$i~onpZaOYpiuzt&|Ps6J}v`=BnnVJtbYK#@#euHa9ysrI5cMpK+=Us^i%># zghg=1>BPymbZRd1(sKdZ(eh(h;8MF?r?gv2^U`!%cET~k7Jz8-v_4e5v4rP^@I zQ&MFH!kD_d`fx=gNMn#0YIVSJB`PpNBwts4%rYR!5 z6hPx{Jg_{MpwR?ZxJVYTK&Gkza$=db+^xPZ?Jih~FpLHv{v{$*$+2G~!b<(el66S0 z0v#Cr)PRSoA;4}Yj%9-C^?nfH=HOK)=LR-Z2rY9)9}EG!KR3ZX2Txa5_dXTu^RBhn zIR{Z6Cp=_0dn_4k0NjDM%je_zo0<)d=g6VSRJu#g0W~Wv|I$%-KtM= z_?j^lme@?3#SY@Ip;?=w^sEg`G!FSIyaG}nhJdmAdvyP4xOAQp!||pFAvekYrOL_U z2oYle*gt7YYhC9&))7}ApBETO8OING8zh#?s&D9g+Qjf}o|p=8`8=!^NCu?#h1*)n z;@{D+S!{NRQ7NrP^dV)YNbwW_b{xryB0*Ayp;C%DYIubrRogFQ+BqkU-vpYZHMVz2 zC%o7jomp!dOm(%;mup4GcEV30VtpeO)`KHX4Tq;&UCwv$&(&sn_xEf6^Z{_j;X64b zT{W3z(wteJ9@^n6;;x)Xwj>-a{j4S#n3VHK&=q;?1@-W4TZ$7pa_6JD^ySr3$+WW= z=&Z$*_W=upQ>Fu56O>7Q z{OdqFu7UmLd-1E2BhiUt*ya~Vp1{a9SO#t3w&F{bNeFHcLqSM#)5^p_yfPCHILwy`iZFpKm9p2RjFxK2Ht3Ri z!#klY_8pvT>N_b|M^g1Bx0%)sS z1M{RvD8OyMdqogc7U2ahH99^aPN3}Lv1O4)EGZ}3m0(t(>eu6>=j-286#htE;6Bn5 zzVP_G9SE6APNA*`|MAyXuanNls^cxDg4wQ-9_WsOKAz$5w~9&9Bp$~FA}VR&FxG&h zO2=yr@`w0taRJ!HwiGhfv&r`0R@)cA|hnP5ve+P}cou@1tB9@ zGGQTxLuQ~oG>yz6iEt$3>1XKtAYSeBu7$_Z5=Q`0)o^EL4+3KcuY^k`Lf7KbdTGYtgmHSAhQNs=eCX617!13U zZ;ajlBzg_5#r9%3&SE^VqdSI61lk0x)Xaog2nE%Gaiz5`n+mCWho_S%w!dopTLQ54J{3lD(0%SPVM!DY=VSbH-Or(KbmFYe2(y<2eX7Qyf2dYgPq1I=DkBSEl zMH3k`!;%#kHjI`tL$S;CVF0T3f<(5|3~3x3A?I_Q-zE|<^POUuf5)gXL2cMs$ATd6@kV6w5a%agGaK%HGAzH$d_sPbzeS|y z=!YiozSyW0Zr)p7cc3Iz;pcF;{MF6d)4}6k@8Hns<&V3Q=gQV1Pfhyy0FTvc^nE_6 z<{_bLMv7>QGflkrwaHWhtoBvYr^Q zPGRj!<^rZht!BE!xvJ`-ilce0mZ3*XP2g84HrEd2nyL$}& zldyjl$=l$vwNH#u=WKnI@?YN!BN@1#Rx^!GXbNu+cnQ#q2tMFai+Si1e0>)a)}RkQ6W_YdbCKKWJDcGc`iCh>Inw+!pi8f4~>(> z$&^BrY`$2|6!i&6_m`T2I`zdo&3yFOjcd>Q=|=kOs@MDV@YmJX=j*w1d$kG$ z-`K#<&YlE}!S}ugUFhlG-)+4e|M2e=9{`Qu1H=yE4ek#`y?Mvoke_Hj`q&|7oPP(* z@QW{11UqMx*Y^?G48&MK{?g9Njt7JrIv?!_Qj1pEv+YhDc+Gr&!q;0c;Huk;Qr22z zfm3p{i(+B1!3PTk6YsCSP=>8>N;Gn7z+calRBn#Vl7x7N4lk43UBUWoAB1y1&0xy( zAa%ZjZQ0lU*1F;GbB&+H;j`8Lr{&qw*1?C%N-OBSEPv0a57qeA!=| z+&`C;RlIHw*ZUC~sb6|NxR18)fe-U$X>XQ4d)VOZt*4yShrpTl6odC2z}t^UTOL-} z>nLO|?=i;EioIe#GmpMU%^ZDze}j6q99s^YRs^<`6wZE@kPUjbFw8b~a#z!fzNeyx z=jnaTr?aHOG6DjHp_pa}L(7+MxiWF&{V4Y42q>4Ku4sZQua<0&}g;7bVtDo5SHq zbm79yokFc~EmU+WR@iGer>=JO1-HYUu0Cq)^3J81{jLtILA;44opa!BYUJ}M_(G*G zbDaowud-ft0h#NfE}iZRAC{P9b~wG;NBHiuUn4O$KAwFNd4}H$NoK=!uukE%q*}J; zJQycvp8Q&Ce50o(hO`okbPl*0&&`<`f-LhK^AWR5AkjCmbJ*}}r^hn&xqC{I;%dc& z0vl&X>^UR_H*F^>$A9V`#u1 zHZVf<({JxmV$t|-M%Rz=9EJ##Z_56cfcJ|nCE8El#R809>Eio$H_Y?t6Jqe~tg}cL zzXr2kv7v`--4qrXCsS?DH`$6phSM4leD0$3_JTMxFxFQ2f&8{L1AGt0%XFr@**z(0 zw~=Y^ap;ZRve9p8#Uw+#LT%kNQHkS^o!Dj>>uMU<==jI(fz|hL z5PFssb+;T-ZE~LG1{vuG^*7~-Ik76sAF2m%;C7KlP`juhY%`E#TNvOyd394@rh%Of z+K_&5De7grMl50l1$1`RkAa}ksnRpnS9&ss1vR0!d9UO{Y(2D)9+%L6_6LGNE32EJ#ClI$eUro7)D=gpi zbRx2rh(r*Lp6>Q?#nJ({`2|nOb4~SQ-*N?&{lfHCYXO_3DtBQaXsX&UFoI?hMhQmo5#^n5M(B3vUvUTn8;xM~;6kW=7Hv z>B0!Pk>DG(L)BoHPcw8|H?gJn`Aenj0e`rc;AivhXa^?}l^0Z0C84wT(;NA4l14i~ zeq+;@={CvsS=5TO47Qd1N~4iFZV^RMASXY2gs++fahdOK*PT+rga$&3?1;`V>w;zXaZgU zc?}Nq+~cN5nA3;|9gHL^`N@LLiLV&DgKBMA(!9GdwWEobnR17B%WjPA?K+6)pOcGv zX$1u26<@HLcmTD;uW$VgIyZfx4o#+I-B};CiA?-{v0Lo_N~qgE)sr%i003)t0D$g4 z5o%!r3u|{pTWf0z8#7Uh|DS5Tmd3xnSc*@J4;UT(uZaT9qO4`j3#t?p#{yE)iNt$p znc_Y&@q>zW{kiRe^rY{PEU>feq^u&-nhDlna67x(?5{lUTMw}=G&N3T`vNi|7NTrw z`<5g^uc#?z9f``J;_-uU>E>{e7i*_>>b=T_fgwo@YP1omqF)*8%^q3Bw={+kf-_Z$ zFsP*x1ViGKM81xmd|s}EC7gp;nxcZExBC52 z>MV&a1&5edo!VMFnWY0&h9-&ay5Qm`cyf##k6k3vsV>2eKFsM{qUS^KVL7jIdH-NF zxHrl2Fb6K%#yZb0EA{R;E2KC?OS2jUI@-yLqJFhspz9 z668pxnmJ^~5ioc3UsH=tZ_j)B&6;2dZtzjrxUa-`+CX(|g^}K)Y~NpqVpI}<+Fiy! zK*x8QW26jd6Tj=Q$5AKZj)dCIeS&DdTBBjek;k#B5lXF^{FH?dz`C}(VVBXUF7PI>YH zENf!knws1|b>&D3EQEI+f9`4WYW7}?%LRFs32|t1OdLvvrnsrF3oB@nb zX2&Yblb-$+t&kd7k{cPH;=fz3N=SY={R7xvm-l@;*=>i6Q)-pnM1gJWt?dFpde7;BaB_hIuS!zbcf+Wnc+ql{q*D z#yZ|QBkF;631ylRb2=*cRiK)ki4kow!z%q~Ob~yu7NRGMno-W;<%jcI3g^zkiYB#ah8 z^el6INf|p9P2nn4!-AA}jYRYk9d}etm%4QZskw*1^}-!F>9l>}gQZ1t$Vo<60T`Tq z_!>^#oCEtU?)bh1iIF<42V=1yExcGcg+IP2lz3OK`d$$zuo?n@vc3$`*tdW6#43$> zL-LOf3+~@;zb$`hx)6l?mai{|Z~WH~f4ebiX4f&6@bCikLM8_}K>eDlLk_}fVkZg6 z$@F?qw2bWG@CWIa@R^jZ@cWy-7Z;>^GYI38T6&hntdVHzUgow zzJWOzJmDXN>@O`$69L~KKPVsZ@8%6c(b zp@kVYg$s3V|FzKVF;kC8IFQbih*K8j(&aRbF#}XXL{gvJ7%Bl6w;1d_1XPXVZ&>6Y zH>j41(JX;FvRcBQ@J08gzv0P>2(KKe^G?12-v#8aAQpaqHN65bk?T?_%w;|jKP}ghEyb!;v@{iwWpLn#n39Xkz`Wn8k@2$g^ZZr%rFJAC?OKGoy}ACF!|M} z5zfejOE<`hLOBY`jDf%+C(uQ@8ikD2exu#Hb$7k5;KT z1z$CkD7*|SZyiv{^Ye+<&A~u#nwuDII-PbLyEnNywjgS<^Vpe0 z*-hRd`9M`ybz5jyq&5QoyKq)s(s{2EhyS8FE;Rcu{(R{hK~~XD=TE zUYG&7?&cs~o`Uz77Jj2%bGv6-5Fh{y4p;aAqi?gO56oL^Ao^7|L7UTH#&tcK9Aqh$ zssuasKsSFk_}xc-ZrTur*N-E{vEv3`w>FNyzb0krhSJYcL>f1frd{CK#!JHbVSgPu z^1m&W%hk@vl)DC&Mgj6?w8k1v7A6p<(3He5YtC5R|-II_-jRi?UWDeY`K3s*V!HP%8}7n5DAI-;|9` z@alTzmH0!;9;Pa9cU(_-!?AkC>qMLpg5O@=b&XRa@$e!T<0S4qOEuh|fzLebd5(qR zd&+Uj@`cl-^vdP-iFC4x5~vh|l=Vf;l4RPnfb<|K8|#iRQ0h5E2Pm75;)Cz!Q$1UH z-|pVduiL@p;qmW1^U12q{prvT7<;1kP1Ek)FFOc8c9@Ii&1?;wFAv|xn0G6(eOq)& zrYq5ob>`FL@UR^7Wo7l&?#@>4sD>W??}06deRv?WthODKZx>I99Gdcp2f!f&Ochdm5Sy&Q4q&`!VD&vN9N^tdwTA#Q|7bqG=am z{~{~aG|k}^{r{wdL#VbjL6VBlXeM~tsln<{6`g+cnMnSwj$r-Y!ci*{6=*`vICf5RZbFAg zCYbl46S^457|RxMZ$74*u{4aSKIbb!!!By^{@-ZOpQlCpNe)CN0pxFo`X%?H+cUAN zFWK9yR;Gt0H|C9qwX$U3+!U{G94>1(pI1d}?PmJ-2x(8sWPHZ1l^QKL!fe&_FDg@t z$N4kcb7jJ_hUpHYF_bddF30_N2!T>M5RukvtCae<0Gd#tL<8g8yp7>4xZ_6v2_ZG^ z5IU~5Q!R;bzq0V-{DbPA#@TgO06xKrlW}L=zxtYRlE>d5qA(5+-}xsJM@?YRB9~Z~ zwGQWB%G-wL*t3LM!J)0fT;~f5r6JHwN66(~eWJDEX;!NlGKfkxtk8eq3r7RH`Xx-2 zsF$APIV1#D6Annlr42SbyF5b6P@vU;V@2f{ijvf?IptIFWg~XIoXC$t|;{JF*)C&}YF;Fb=z3^eC@6Ir@yo67XZN zv<%+g8=|+yFElAKncUDdZeJ$i{H1!`wp6uIv@2(7fa=i80FiXyTo!P>AQr&ei2V6VaqL=x(rc*sO z8Cp&U{L}~L>?P!oE&OU`mN!Lzabtvq>((4F{?y5NVbKXgdc+Fo?H`If)G-`|$C1&M z2zp`AbMeu=Y55ZvK=TJNjHz#Y8xI(@H9!$4Y&WTG1XxjW<5xGX^Q*TFU_IsW4;*Mj zI6o#(KEg^Ysacm-tF?20&02r1M1SGM8tiq)aeD;0aEt- z=)qtgZ^Uj+UW)YwZcVMBQhF?rneTz69u5lf053+kQOZDMC`>4{3vI# z`CsI)K41B#4}!C;7A6uwqXV^<4QMq4n|gqYTn;Nm%z&kFDcy9z`3;Df+CXG^+3m)W zNsfU;&LO>)c}jV+dAe86@5(tnTjcb>KTZ591qK+@Csy^F9-D!8~ol`E-expch-^o(@RNp&*{|PD~q-wEOY=x7Y-U%d* zAo%0rzQNLC2$t}N%)BfV?67!k$oySj5WhI%dVOi33ObjeD8$Y(N`7I0Qb`G#dxxV2 zr8q)9WFX9QbTCzj-zbWno2-ZKj_7W?G%pT=C8kUXI4-0@x^TX(!A(PizLrBDJH1hD zU?SZWrcO&fAbMboM5TLwZOog-I%>#SSnK__oxaawX9dx zNrd;k^&M18;9o(zSBB+fD&U`*M*5!9g(Z2EH0e(7aC?AS)?Pn<*l#aNxoGUB<<79s zTd7zP+D@%1U=mx{@&Vdwg-Nrj2xMq?M!By!PO)h<89}z{NmWa|Zk7Wu#I~M%0yhca zlt}|_2E2*}eGq)Uy1BoD>|TyBfH6g{^sr6JzsbCP5nsd#7D%94efRWR*>n}~WGGIu zAP!Q>UF<_BjP0gBWE<<|wpxb8d1g}AJl%RPt`u$I9{iPIJppl@ywNbMMm!}O{Mx4V z-nJRPFtYbhWDeZq!V@Vi*kWlh&@z>Gv$w^rjZoi*@S?Dx%3~!(ef(EWx$R$rhC_9G zcy|3$IeB~P13%-DfS=nV(6!*>-jDF`STU`XMAW4A7tdaUXHu#BMtLl3Sdb~#C1-20 zt~L{sg;f@;*Z7j|j=!CilCztQ)+b2#Uy^|a)on@W{s#PAK7dLTTBGJ5Ojy@~)y5Q9 zfId4An32ud=S#-$Fxfc4zt^nHX+wfCe`tZxq4wApBPS5;y7%op^E;C*(6;y5cM;!< z?G@UmbK(X>Tg{png_aF8dNNp2DD9(FuxWaV9dtsrJ>75`bkJ#D z%e`--s}MzZ>kIP;x?)2+xH?!_Y2X->B|YI7=OHI=B7ZZ=J&NVJt?6o7fsOHj{W?;u zLx1I%PVeFR1Cdn-ZbXlS;-Z?Paj?5=%zUX13$HW2T%^nMKv*-`*m#PL$YrAnCW$dk z?Gk#tczQWN#-GXk>037Www|l%VB^Xu^UXn-N>|_@W!5FLk z)y>}Jh1}j=XG2xrq3%a#R!{Z0MfH~2l31RSHHwHOaUQauZlba1d4}QrS#v>GIKx5O zedA5JA8~`BsPXtKK!Q$UW@qgQv|66)Ft!d@B)q?s(^93!OPDZVj zetw&)&?r$r&*c)b;I)lOwb={?7D&Qw6;PAq`h_8 zj&O0?U*krwQzo4vdNN>Xpr(kUW)Xt#*j<;|lyZ=4Gr`>~nQG|5?b}R$tFj`Ps(7YZ z{zZjn>%_}7bSYqMre)m*0HifM60(3o}t$%C3d`r+Aq zb=-s5w%c!_@NOdbAwK?4n>SW>;Ud!}v)S6e*ZARDS-PdI1xu>TqM}8|qtdEqRROim zDrHSm#v~e^5RZ3_<9 z^`{Suw%ogqH#!>PMtQXe(G~f}m0Izt`ZQ9teKzqnVVE2@R^iS^E&j6c{t>bnh9dU@~!fo8k) zLNSVzH}PeHc5%X!CT2qwP#{M<<&vg`3nf!ZBpv70c5p#$IHEFclkVBEYcKBg-vDAA ztvfA`L^4ToGxSnx-#9L;>SWawXC(oFmp`56*2`<{)g~Q1u6I0BB$x9kx9r^bjM-Jj zKs;$j(YgmW^W-mIOB5CmsnM*NlBA{1)t9StR(IvuZLUzBSvvsCzVrQjb|5$QD+akC zy?oC*8{*;EiVoS~#ob$;^0SA?<9DNk$y2AIms3xT2#%VRUq^U=A*|s>5^m;; zse~#bvA#&AV#XQncQ~9Isd7>}sy*AY-cERfv?x8OpxhSX?(Q*$@P-Fv(ZqGqLbos=-h1dr zViLImYeUH|e9AyQ=w2y-F&+`T#m;z3gRC97Kt>wcr>7t76;lAMAMPEHj%>L<7bbH1 z0v=~tZR!jSgK;Xi*XkiVe$5MIyP)~IPcFjpl1yCkjUY9$iIOHU6b4#hd1E3f?o;v- zc!&7CHOwqP9OtR)Z-O_7@OV7|*|A8<$Y~-81D@kA_^s*IU3FHGyT`b1@)TUCSvDI5 zJNP!}9*hb7t7BEv3q4t1-qH*$bMNTeM+n^HK0(G`cnY!&L2yF${RNEDYevFBNe4nN z?oyV1eAY!Ul<=HlLl?$X4|wU*^M7AA2-qd*K&=xM#*?jrptWsKBM0;wxP}JSc@c%e z{@^q7Q-MJuTNe@E3NO`j5LAgp;2@Prmg)g3iWMhi=0)4rYz(m41*5p!9p|vsuJ@#WtgO_5|wEzZe_8 zxP&=_RB05m{fdjq6O?f(bTR_a^6w_VaNQ{J!hMBS+{2&JMWZIS;fA@t?3Zc%?w2$B zUJDw~KI7^3@L>g1RG#i-p*~PXaYFi?Elmj={Tg8>bq)uRy5JNNn%0mcl{saDuKYDJ zg!GxBb95fO_|sHxVR9J?Q7)8x3-4xZX3UFl4; z4fW*ppy;|Jlz8D5Jwso=x z`2ogttgeV)H~x!ZK2vrwfq=6RI!bU-b_=9wx=bm%9xS--z$IH?Z!$s#p2k5{CSaL< z&gohW#uQ9DIM8)b1415*x&gB_bBK)Gkb@k3Qy&p)066 z9Ow^82n1l!9pfVL;2{tVL=}orBcY>!VvBV9vPU*}L3x<_O|s^!QrMIuTTdo%q@qAw zY*>)Ef<)?T`%-$gTH$K%^7mXg-#5qNgf=TktgwOI)9zP3&zX7$E~uHV5`Z>ooD<K};*NR+6Un~vmg@|=s$`W&S%1Uti<5{|UvapZ`z$#qewgisC zc0t4%yhwgZu0dvgR=X5t2K`4m&XDT+k*qa|?bd_i&6#bil-#;DKLX^`_xh0aiYmey z5=Mc=2K*=3Bau`}3DU^}Czu8jEpQ?Hw5Hg<%z{}NZ%}T?prJ&jtZ%Y9asxUza;V$O z4vtP^n(=b4+$USrhd;qTo@_qrkY;nBE`U3 zD7{xaTifgF`Sb`cK3{Te&|nK@4kxX3dsMp~?wr$%xwr$(CZQDDZ*|BZgwr%a$*-9!^xi>d;^U{yq zFJ0BueZD?l(Q%7@@hR;(O?A1=QwMmNBsY#~ErQnZQwl3Dlhe58+N5jthY;rh!OZ9I zoGGHiYuAHevWH{ved}EHv|6Ku3#EzsOd=}F`Ud(heqP5b0;e^I%2UaeCAPVF->hp2 zYTVm42f0?zm>yWfRO>%!jNCo*t4U(LS663hYfrk@QmF*7-e-)zbPE4&-v{gqABLET z@vs@L59fMBe7^u0wMbcX`*kD@Odrb4q(-S)t*L%+BjaoTBEG%~!gbFsGOd-1&I;Lw z1ziyrwX{!e`LLAaMz(dm5?j{MF6t7EwT4lrd^>|d`L(;xM(zU94IGAJ?UFvF1sdW+ zHYbRrvvI?VP(kvEH_dzJDHM&xx4Ad${enb7Vo7Xoyya0CswRY;vtk6W{Aq*Vy|<76-{>CaKYfK_exd-rjoVH zwrp9x(%GMQvrU=K=)JzV)n>Nji|-Fk$&-8-ppy4g@`_nas!tk{2tq)x zEVVTq212XWWA_wC05ZBQaKPoi>mYVrsjoJvG)Ovz=_Ra(!%+wS z?)RpO8Kpp+0HnBCt>H&gN#CgOm$^GXR8$1m+>g93jy4@#u?IBFtSSHQbsou&r^hb>Gff5}yb$qx^!3*3{PW~Vmdb4IXy>v+k+Fp{Ma&d_w zqoavX^>Ie5vK%l!uN?GGMS>V?3AU#d5nQ-*9nn*gKG?jU*&` zhjw;o?;jV$ncJ^1n|@ui?%w`o&tz8vx>a@N(zoiy)z$s$IPP24FS6{dC(}*DDtD-b z%l!`03+_wB{WI%G(GPeM%i5?xixx=nVRik;kA{Ni5b(F}KLoqm6=15>n7BVUs+=BQ zJgVT7-!`h%UKLS^TY381r4r$d5J4&_Jop{%5O&&d<2Og(yC*l&Tz`% zdH-8)gi#Q01t~2#a$@(uaCd!=9slM*y?Pi<6icF$DwQf7DE5h0*l(Go!mT@)P=rxK-pm6;W43`o0cb)W5-Ua z%*~RbdQ~RqBpEn=Oq}c-saSQm(9S;pPz#5N*BcHce8KGPQQ04?LQ)T~b)H<`?BRI_ zY+HZ#DXBJ8AmUp%@}p=%m4WD8(kAlcK*ck=1MUu!feTB>!k@A!4XySWjJrZ&+tT?e z8>V({ZtdB#GEgS!C(=LC2L4Tad3^Lw=*E8Y&M7OR2-NzmJdW$Z<7>bJ+GOSfJx1;m zj7&>TJ}f;QA^N8_E^m=v&OV-8CLkVw(})jRacCgf$WK&f zytZ+d2?WEf7+g$2{$^dP$BoH1pU6qzGy&3qjg0f#sP-a7W-y_Y@EXT-Bm7V*FQ_>(~0*>)0bz6lhNGkONJ#UC@BXwh#5$QjdlO03C_p^q8v>RS|rmz z_8bgYH0Cl?rr7N!kma(!md%Ej6B4k@WjGLz07yU;MKy)vrx z*|0|*bL`)3D#FPT#O znI#=u2y=p)MV3Njo0CA<{tff?DJc{mbSKS%=t^i-x1pm=Z^r7)7QU_8aR272>soM3Y~x1X{6Sque*kdmGMrKk{`e=&0L#eZbb<0g!eC8-0#cR+k4>l_^RJQ zt)`73!6-19P-d|Dd*z;t?P&Qv)Z2zf7cN^SwxZ^{2H@umEJDAd!671X%o>Hh#pK!% z|CmA8f18CMFQ)mv_6k-CoN@$@6%SgE%gxS+^XqTo>cNU%U7&(ji0by0wdY!K>zQk@=i~!) zG@_}Alo@I6KVH6J#<5s=at`mc zuXMT7l+S=5spP9(^t|G?# z*Wa9hCvegzWsG|c+y}`MY5y*w8-MB3Cutve@_aK?XT@+2dBhmbX3ynLqd43DCcA=?r>NmC+GAh!`Ip{-)G6k zm7Bh6wFDH&Q|hX^i##m4y?nM~P94YPBAK}tJ=6KJAf}mqDZ)l=O?$3qHkpeYSpk1n zQb96VD#Hf{1p#6c8H<+pQ3;WNypgcaEoZ~SKGt-d0J{mQfMza)DrH!viUlRO%T=UA zr*Qvb0sszX)jxi0yeSZTz7Za1Oh#xFoF*CLT7Cn49+MB#jNUJ9p#oeGr=lR$?tw`r zc_cH`2-%2Q*Im8qb=zeOwd-8=x+K?q7>#<`ieAK%}x|f`T|p2@LWZ zRsL=j)GB1`fjT&wVGEvME7Tfie}PlLpAK5Ib(gkch$K0^^O&8DIWllE#)n9w3UuV~ z^jP|N08A6%+G4JGt}08_6CT^rDHYh;@kkLFx;2EhS9wiBi_-jL-yV7NeVL?FP?QPC7vP2A=tLzPmGl9~2TwxwORIWgaCeyj{52wZjx{!x9@5-uoljI)-Ijj1> zr6`dITKE+==eiRg<)=&gStZFO6B9gT!3Z+@xyqD1BN}OX9b%+1lJQKla?3+yyeZLDy-1(@V}$ElkRrRbeKmfI?hM zY}e@qo$0#k=s=MDeYxbY^B|%!R4q+nG_0BdW=!R6s5A+NQ=7!Q;-|>Hui%VFl{QCk z+BJ4|y-zE&M*^H_s#+MV*ga*zojtVoxgJPUuv2eNhQ`kdlbn2>)j-}|;YT;8{*Ap} z%#}(Wr>Le6P_eQUSK@qSoGh@lN2gWW9pg?uYqVz!llmz4@PBPkcg}v=W* zodol5Onw}bMJ))0sV(5Z;|`5pjd_$6yL-8UHZdg(ur}_1j6i8;{=U13;`m#ImWzr( z?Oc>_#4nYnn+KaM3`uE_*=i*?bZyghdFyQnue;x2VFJqO#*|Xy+U{!maY)uGb`zJh z3WJGm5aaX$I)hQ$B7!`({>FiotF953rh$ZG#+LIY+HGj`g)bWSVi(bXiq`xYrpwAGpS+Z2^2*_!R?js8;)6Ml3ImpQ_u{k~f4`KU^lFBh>3HwZ)nIV9 z1kS;76>UuKLaRkb$2`>qwwvN04F;F4Xm3r12JPK97P>IcbbI?_^Npx5jJd|$%4F>J z+3(O;M7A7t_{-Dxi&fkb!F@ai0p!T`?%$WIkzK>$83Pq5G$w+Z+c9X3Z@k(->U-j?_|E)wU=oTx`+%TmsZ| zwSbbOz`fkE8&^VA=9qFc%J#Zfg`{WqQ?)LbH`3VN`o6bhY_!_nPQUipu%lWwpNT~&y7=#*QtY!6;(AB<2X`l+3HNnp?TZt_)H@#^ebjzUu#38}@p`@in z8bX&|8+g)r*@E78j>O9Ru>1{u4%Ke-B0fZEU+jqf$UmG+~1SgNxcYIy~A{2!JbstAOs}YWeisob~*?dfBYi-Pwstt9!f2 z3x$(O0Ss(TFOhiCdx)BlXI^-RJo9s%w`7P+q<-83e zWD5Sg*dWkY1U>{BAt%Ao5S?`kCECXMOi7joX1eIc*@A@MNkYu3F zB}>FUnwHJE(>)FCUt#&xo!SSa>HS` zaJ|ECEQPIc^cjj=XI7TgW_8khylc?)h?TyScUqsEwZz(3JOADxaYKCWwjy>=%13Xo zuHMAI`-#NoX1RlvVq{+F_IL&O5z{YYYO7zZA#L2R+^Rp_54zSyGI@LZ+AuXNha51r zSPy$!8b))EG$@d;Lg{uvMwE8i2<{eP`zyaxMHl0=8PaC##F)Tn^L?COyq`=CFONe)^n&W#7v0)iY-Uj^pxg?X&uy(2$XAmM zH@rn`H2Tg zzD?QAb#YFxC)|vW??jI0k_?oOKU4J{VXA9yzu{lxu@t|iXmcdarJHY3S`}AYw5OJ@++W9ndZ2#l>RxTiF)Tink4ApY zw+Mbc+m352lXCq4=rlM8<4@=8TL?d>@hMWXJPEC1?~U5Q>aw`$)o*%J1$%jz9(DRg zkdQksrEAN}K(4!K9FnUOoTd{@$G)ED!u<8KsnvfhWpmojNQPcnN%AgWQ_zo{2BpSp z{p{!EHGcd{c4P|Wu_jjav%J}UI!kT=%kPc? z`i9HK?SicVi!Ea#JH;w4tTSPJX;X%e9=X8-E~-T0On-w%;VPbA4@;A0hCG<@l`i)@ zuu75^wFh!LJpH@H=FG#n)f=hl1_v7x599llNS34;OtRMx!XN&ce^OGUgI4{{!oIee zayH|;InP`cIC#YOW-iz7o2{3ZNx|vX`1vyv*}rbj@$^5HM|IT&56A<6fVloW6Jh;V zz+P5F_`j$0>W1<-;)q`}wHbFrY!TT+6E=`9fK1y+@ zl4mcVJpqc;wL@AL5^6{-g^SsfD*tpiiOFJzn~Ro77@7T{H9fWwVoh2|=jSFsh?ldd zfR@Z`Ac88LO`U8{m`p8hXHRDprAjo7ibn@8henu~i%4WKWLBMauCU4|H?4WwGg3~P`AAvWR)G)S+;-VGR1L~GQ3u^+ju!qn*3x== zxAF0mbYN%wkhFEE15MVSPo@PG(p=np9dS&G+4L9+l;0ZkEJ*BeuvS$>K$i}qHjy-nC42G4M>5`E5N53_?Fo~ms(v(Lx1&8mn9AGfbM$G zQUU}Ho zYCwh_hEk+|4Vbr<+|TL!3RAZ3?-E!?-R!0+Izvg@!S5~VK5BOlhhnD;{Fkc_IraYL z`QtgGqL@H=V$}au!-O{8;0DuyEE=rD2)@uY>1=8mVRnd`p?FaU=42?BrDbXxA{=cwLtLpV$MKur_~X_6r|xe zxq3VJ$1MZ-T}~ z^Z4u-7|t!JB@6P?=popB{6Tl|WT$nHZe`DOYdiXFBBvdG8eN~oaQSp_-7r<`4PW<* z(0#J*a^>DfwWTC$W{If;&7MYkt=rkNC%U%Aj@YvPo?uKj1;Fb&fybBcX>j_uEao4- zqs^^-@txMBmDiSBB4at?AjS~2VQVp3Bf8tFJoOElHf`@@Loa=?w zQ_F0i;&XCy{oLhe4Q}5Acrb1oRM50!MRlXR3Whw$lwVH^vzM(ksqR~BqROip8xRNY zhc|VB?N!Mpgd!V|CmWO?$>^dxCdg&s@{VB@$2^Odl+c>xLfRCf`ttterf_?a8fn#y zep}*$HP$wI1qNJ*mJD}P)eON3jGwa(mYdQyfbeNB6Q};3wEB(aoZ;@OZ2BNdH{n`9 z-94rT71TbIs?*inW)UXcIKb7*fYTUbFdT+RaxWeb+~||oC88kgV>as3+4v!~)pu6h zg(RvOEmjHC^qx?m3il6YOseLN6RY-5Y~c#T-@+w0DM@?iEl7!Q!VsV|j0C>&e`zi~ zT}*a&KY8Uy3~*9azUAs3>~IraoeyVW0PH`15JYR50A62fCTI$tP9#Urh|4(15LwH zRg?_r(a55Mr=a=Ns#8y!X+lfWoWy3+Ffr7ss^*tfsWeZqI;nvMDT5`OPgap9E_}J} zi+Bh3@8tdh?3=Bx=tnu$@I}-*f!kCM^Yutibrw}EMH8SHd=bT2eR(oeR*FTlfV(JZ zMoDinr1HI&m+$E&$)?>q+S6$2MA}QK?jt{&M_=p^l~S+v1;1z#872ko;L$C!CPP)R zD=wDspSZ6p;ZD*(B@sRQkEQv`y-h0;B}=N&8+tAKM9gyZ^dMotB&6_Qd?-Cs7O~os zGc*(e4l21aRlgiZGed?WC#A+g)>`uxK8g^i)u|G=_||WiY{=A!tMbJdnNDbh)2`P z3bYh9#+rh|zG633Xrx#WJ(Kw{^Gg}#nxdHZt4pmMbWb=KCX;=8hmhZ5uK`jdOk#pe z;oEe_j-^fnh5(F1@@a3U;updtnL(g+sOn`<1~gUcW0P<8{vK?Y0aB@|+aKifm9$v3 zYS5}iI;(eZ8`0|Sd_OhWvw~u~I>G(DKa#3Bm8#_|29ML*^8TPH3921!L}RyobCd?( zCrmJ@!6lbRP2&$7qM(J+(0)Gw-$C+y2OiX4gtev%OPaYueed?WMTt}=r&8&%sTCYY zCub}(J2DZozj!?wdpQDs z92W*fvX{V&a{?>mQ`&^gA(G(Mie3byt1GO@$O#?yxJr(|17(tFXD@K->gq`Ar!W&4 z^m;yJg=#%?D}@tw%DGHro?p)3-qEROh0gHf4cVL#ey{tGDKcd8PZFuDpymj#^MwUmVqy9)S!l4M_Fmt17HtE)w2gp(Png2X;6r`Y zPCVPiU@PuXc`8Bj0+E8_!lDSMT_Ea5$VWx$bwmFEHa|D4aisdv zadLT{g_NT~85*2b^Drmi-OWrhWlWkIqXpRI-)xJ`Dk`R8PywpY#7y}#v`MD&_Cg`; zR!;|g5r_xDlQ!6h$-oKQN@&9g zPKA@W#ACQ5){2yRs9hStnvn;2^btVp?uM4lsLg++T1W(i$$J`qL-dH7 z8ysjJf=fg!0VDyE_vm3mXG{iSJ}~pj(mgh)9Sp-D0K6;G@!Su4D0t12@j|^5?r;H! zy35b`?_8dvqY^>^MAAfRB{R^U<(UKuH{$oz8FsowKqEEqkGU(niPHlF)IdDh>(8*> zj2@O|Yf>BeT;#03ah($ZGZJFy)>_Q22QAeo=!j5mP_kfI9y_WSiWlpn1bhBU>Mu3* z%-8qi63vs^zbQYqyo_QHmBY6*PKVe!sD9lX8f+-Ayz3L(;QbMKC=&DTGY1Tyn3Hx$ z-}#jEHe~_m=nm{FM8S{ok2xbwC7?U@6=n_NnZ@3aR`SO52@BF<5qdMsGW@!jMx$>C z@DgI%A4FY6fdu;m0@q!%)}1B$+=#tAo?j=G{65{C9~G@3LiA|nfs|k?tQx5OW~(O2 zL!z=E)D{8+unA0A7-7r8Np^EnOCNOaUW=})A#Kd5h zPO+m=VS~2NR>(2eWl+@67|DT}p_nM*V?+qru|j@x9Ei`^j`1cM#YF~so5qsUrTsI= zro#eXU2pzy^0C!0)o0Jt82&LxM5PT5JAZW9`-RCIz!~ZpPOF0d;T8imQjO=7^hZlW zqJLlsq$bJzHOB1puRbQxp4{AnEFY_I3MzAzJk0+ zFEHOz7te7k0Hkj0FO|a}G){67CH=I{Mjc@#?uTpH%fS$2GRlh#4x^cyS}azbwB|C9 zhp2fP)^M-pJ}36nDw;-NE)_0tZ7}GsDKO;#{2We#2hah9b|ljV z@@YsJqhO(5f)&ES#$$iN36e#aUUhH#QGLU>3%cLrZjpiwaT6qO>?HhGE&)Eq7vrEs zluH3!m>@qzm424zH{UEP)>Isy2GNV&iUKR?whfC*%#u_7Li;Ee#Z=K}l*HjT-KZghfE+&y{Rkim={g&BFlA!+03Cy<>r$b) za8emZ(2x=2yiUA~(qxAb2V%dOtY)wQJ+@~AWk;ikCA`R+Hug(gTg6vu*mqh}$rRK- zkrIbJO3Z>-kRg`OiN(3vK*%sz>_z?nCL2w-SxTl2w8xvYPpD88;8q6RqbW*FhpPl1 zHej15oekDyQFtBzPXHc!;A>u#9#TTe%;pBIFq7tC3bHF|`EK_&FIax>wM9me+NH7v zPx&@-48ckpB^-DvMC6)?Db0_F76;VOi-9tux|ytPbb;={+&UYKt4R*C0kXv5=uM!m zxsYIKCfRFQVzdHd920?UCwh?qCa)?o9%Ef?1(kobFg~F}V^0VvEHWhmSu-c5XGNld zw1Sjn5QpUOeHtrJGVGY={Ok~c~dN~Uo#07w0XhkG!3>r=rlWpqo5kc6C-_*AiI>Ax>o=*?k5X3auV3KqY0S6f(6H+(ZBA8l;O{M-BA>LT{GN(s%ZE+S1Z&{h11 zzMg9}(HQ{*;UH${kRs3o-))yAgjD0IuM!~aFT6wQ_A>9>TV#5_FenWN3fzPC$Ao;# zrWv#GHK}u=A%-teChIiGoBrFmCOJ!pr;bcOi}KuG8;wt{hm=nuf`|ZiF2cq% zM0}P&1L;Jb*+!3SwUA==p?$%)Fm7Mn$6+C-=}m)>R$M(z)4`|}QuqG-YzQ)d+}@m{ ztPJV?=5*9jZPjQu9|97BhRPj@9J|3hv>V9cTrsU^=|IE7Hqeb{UK3>(N5MDYi_Vt_ zIz)O%^2J<*l`+|p6zP(l_LDvCP_D}=QK7X|29GdS^M&&26ieE5>azZ2 zJQS%!76*1Ar!v#!JgB?$&&?JD2{@DT0)Kzxt9M}Mrj6hOIm|j=%_JX(rIGjNGi_#~V<*y?ecAi_3G0Utr+J= z=VqU!dGk>FnNN~$1dV9&NQrMO3M0sY!01jQ?Fi0-gj_#DbrdRx9uW3}j28Z4VmwwN=KOxq;&RPS&QyNL&jH41 z2oMlfUSP^X+>yA}BH@Kp3tnd4h*isI``u9k&UpYxcOAj8Hxto77n*M|CDLYrVn6FW zmbGuf?}0h%rZuo#W*K>DF{ZSE|6+cdJ=*2|bnPrHsSO|+y4Z}dV$?RuEe`jK)Ar}? z!R7{I@*Y^Vd3P4(Is!7kVbXH*>ViT<$<-xYjca=R?Bt)V6*- zK|Iui@#kx;kk!SSn~XI?GTpc2!5#vAl{JW%Fm}qT@`fe}gw^)~W9o)oVaKo4F!Y;w#j$R|TnBMaG zj~qrjRgC#YewB}b$qM2PV$5b182j$l*-Mi5^N{dbcXimSnum0GN2O(R_HV1~iDoN= zf$E~O5wR@M+ljvuIfIFIjk$#emcdv)n~@Uz(76vH)qx{?!Sdv_&`er$wh55<4Mb~C zf%m?suH4jZ0-d)6ks914X#QU`MQ9W(wg7kl6LH5*ki{Pw`m67nb#=H9W74L9zEV8$ zcZUcsHY_PcbOuOSHxr8jSZu6>n<->-6aH(kIyd8InscuF}%T?I2wvjLz?ZTFlO8ouAG4km|x`^>Q2=jzu!QURHmtBrFBgC+3|a`R@?r zKi~tMdI+8@_&vsp4=Kpc*m~J4L^YKo_GWi{jM0GcYG0>h0ef^?vQ3N3zBX-(Ya?k% z2}$DQHc(pg8TSjOJ}FLc!T}6VVgz@&WW`7RZg5@#7IBZRbVHq}yhf8T5qn^oE;EBw zc$tk!%^+nu=v-gf7wksYh1E7~*=(y@Rg3m~zdNH*1t+Qd{I<`pda|H>W!|582!-=k z?1Hox%j8;^H*PJu;%&W#zNr?G>hbQ4NH=BbW;+3G(xiVD#QuiJ_HH5-#%Gp_pKW@} z<|8rNTJ;JYkR(W=-eR^}>K`T4^Ye z(qySM#4nvEMQ>9W!Ch$_4<4$rzLf-a(-gSCnnCrdL>Bj=+XB>k^t^n0me_i`i525l z%i*wtCcTK|W;tCIRk)$9=uV6Ugfk?Y6b3b8W;Vnx<^Hi-%DG)3>Od3b*O4T+e22;VgMK9+PF8rw0 zYzV{Fia4Eaa~W-)q+ z4Rg=3AAGjMNd>5>L3fx>lN0hFaeAA+$ztbP_Q;GW2&hfD+Zy$=AqB_e(RG=fEVY9x zr=!-XvMmZbKY`Im+>zd@!o@@oOKm@IcJp3&KZ~Q*8)88^Xtmt4=S=SZ+L_+rydM9< z&l+f6HMhX|(*2MbJ%c&cg3`&N;+B8Ca6Q(uOi^{jsbih-#vlh*3AN!eD0Y+wZY2oP za&FXiP7YIF)n@&x!LMT~;KroBm8&2ig8%x;2k_(9@=%&%0-+t_Ex|9^dOv=?$r9+# zJAA@0;Q8LYiyxKc?#DrRR~x(Q9~Y~?b@SnU{Q+?1b=brRef<97_PH2uxkX%4zIp?^ zKf&0(0Ze|DZ(qekj+k(Idb_$ru)~KV?QZ!*tIRLoBhr@N?f+HWw{WxYvKM{6sh-bV zzv6L!=+^Z%d1A!N(Z6qZ>`kR^T0x(JaiLt-XY=K5WeL#)&)N!B{z8 z;Qqjh;2HI7EZO6h<0@^q-|OI1iT7*Xhj}C2c{!X#1I;f-B5g2Snz)f~ycqlA@exm- zj;^nfxIDMhEagFjqI)S}zAlC@b-ZdeP#BVR=5BKaooozOp(&^GRJ?|i7b%0O#E)=0 zWw{?_J}vn5d!kBf^&7X#bwo~YALmF6*o5c;4L!jS)&6KdG{Xg4z@v?s5#g>(67sdm zG|vDroDhofTQ6NT6hqUYCtrO+6p!OOg=d~xtRkKg157FKFG>AQ-6!le5t7f*{?O+b zSVY#ilmX$FnOb4=2yTV2MZP>YaZr8ktHd6bG)5`*#^iR49hJ?b67dX(AEoo@nvl^% zS}5;M;Oo2{Ra7m^8IV5Akj~#Z(e>P1@D!n&Fe@oWI?T$(CobV&Kd~%`maw^#Xl`D; zy;Re3E6EJeynqjqku*b#1L&%i{o*WSn0&+pdP4Gg#5Prd{}?U#GfSc2}sr{-3Y?$DIHD>lKbtW zKYitxc(#xoqYJ7LXo?Z|v=`ky5g6YWR2m1J!l;79aEq(0Tq+0=ubV8KH|c=2k@XI8 z7?|urDWB}=HnyBoELq4rLJT_#F7&?d1Qq>#xF3LjFmD_Tp zOSCGq+wO}@Yz_CerO&m~ub87_=#XM?_{h9bTYdL^?~N6Gc=MO(kMXi)Y`(y8&ja84 zth?J0s+(AVk{+Bu?Q@XlKZz*sbs|yIk4b5Mepbl0rA!fWS*&>TgP!%Xsu_o@thtuX z0g?zuBPVZwgGW#5E(cDNqy(fhGfvzowC=4;>+tX!L`-YiJo<%O6r#8@uc5EBYCIog zl_;2bcA;!#{Mo>R+3gvaJS}M1@U7s(th?i#3%uC~Gm%t9y>JFdbA($bYeuVe%EBEc zzr-ACd%a@CGl8EVQRMu5_;u_u3DvyN77&@e2+hivYgkFzx={*LEx;AGNapA!r7v%7#O7b!zQ&7QeWv*_ZS z)sgAg$FFk@eRC`(*$$@#bp(FpbWP~xoxG(m=tVawe~~!gO>_BCSh`Th1`T9!m!fAf zY~o=fB>T7`N^4 z5!8bW6>gi9`>EZuaK@kyHLk+xhK-Kbj;XX1xn?EfGZU$1CNz<_@*rb=2`A-2!*^9N z>a6>*OZ2vwBj}_e(GmNR5gVNWPm8m%+x}(;;$~<+i%wt4X{Ap(=&aGe6~~998{Tr7 za=m5lhUR9!&XG=H;4Nwke|BdKdRio&!an2wiEB$ShT?+M=uU0ilqqBHHclsP~!t^FX$>b1ioQ%wVza zDzp3g(A)nY1RLJZ;df@&042>gw@CH8kf&XPY#%5b4TTq?Md^wUdtZqkwn^wSf4xDd zC%s|$fHC}lm-PLt>aH-L=332ry=`SV&DzFL7q9bJy#&)WndC23mSOrZ;v(AGqSw-1 zxj_3F$HK`wzNhaFi9YDA`ge4Y5fOQ?&X~LMyyy9~ANPA@ij40qWaote)qDA*b`aR! zA7>$9Mb@Yr9ypt)fq-SaBVGST>13C!_^$=OBNPX5QL{w7=6`cx>BlyVH^_ z$=K%}k?3OE^WFI+#-qCBsv*tJdn{u9Tr0-QB0giW9q$tJR6^Pk0dLJ*RyM{dw1)GfGZNut6u*U%U0n%e~sKwAD75 zFhLN%2f=g^Kj@cvR(JQEHP*Hg>BVDmjF!sYwI%w{g64KE2NBk-ebzR* zL?xmflbFZE)LU0%+sct1={^{X?7(C;IR`qNY_`Q|;)iE6vF(1JJt&CaSDZhrbTSwC z^%az>+#a5+z1O_$Nzh#{+}%pJnz1vvP(;({(u}ZOKW4{aqQFi_b90$W{&BefJQ{eV z#RcA$SpIKO1&v!9*L6Eg<7k)tJ-F09xTOo$dN(ay&w`%No~Pr$H?9UKhHKBcq z`L#>>r;#0jPw*ZX3H)wumc;ba6wIPtSAA|_MyBa!5A~<2KgI?V)l3}cx$SCYt}*K^ zpe^)fb2X`3A)v)^`d&2{vb=PwH~*whfQ`=zN@W2O(MN_etv&i}xRzH)4CWITI;r@?(THFSu< zGGT|5A^%kl0tAak(b5opQm=Q^*q*>}5=!~SvrS<~=b~Ex*taK+?=io6DTCfXuWl&= z-L>I$&%FnEhSQ}|*B!87Q}ANkb3Csm3oy}-S`Z>_xu z7yja9r=rrgU69dBs~yA-!hfXCl*+2|;{R;S%>Qi6wEq=-7BMw5bhUB$-w;~N(Aeew zheg{e%f|oHGIyV<$NcJ#3ahv%&sO4FDx;JL*+vsjh8-cRSKhQlSN+)KJ`AB-W>?~~ zneFntoi=$!><6_`3EVJ*+6mw%A&sRxOU+8Y4H4DIQcEA8DKgAHx`diTro)Uipj{dv zE`^Tg*}>`5H!C6mjff4 zP;#_X3|weZd=a6QGL$YRTehuH8*hYIv~0Sm`ki6;Tt%wC+t!B9p+c4eu?Ww6gNh&~ zvmfcuHc|RYA{@(U#+n^R?%by2Mpu&`8@{rS$J&Fs=20lsby1=ugyB(oRSkG^#1xpC zhl#=vgvwa-#e|%V^a;Pb@_jI6zwDq#4|KMv%HAfOVK=c!tL203dh=UC;}ZA#vPmUN zk5@;8L!ucO{YFWgN!r(v>xTpFV|+NlnCn_Sb{(D>U3LDKnCdFd4FrWWGs$P^#Vgy;fu=-QTBc+KS7sB!@ z59H^l|2E=7F8+rij)LgG6%snWgjdP`yhyx|sM8=!w^yq9fhXlKdqXtWLh;a~%xJz< zEqKIE0xx{=O*?S)?El!Tb4>m`XQfXN>HmU!pd3Izl>gOcwKH{LP_{R=Hg);GKYdb6 zHAtHdz{`@f0;jjCBh6;8OQ8Z6Y%Jm{ zSE#A}3PhAE!TLElbM#7WVOBc~o$Qo=z_;W;vQJ2f2v=#9bi2wW}K?|!ueVMg5ohu3{l zdC2Aino8_21OE=t>_U=pA^RxEMHO=@^%sBsfU#bHEdp(CG? zIFY>e8BiMXV;|G~)kG8he0{psZ9v9ZA-J^hxzXcmiPpwl9>t|OzP=KMsAq|4{ZkbI zRI@EYC4@Hg=7bz+I15CVkxvLh>{wvAwBf|9009FZ(PbxRl^}0_VA6j@f&6(<_e~5v zym@h@!XSSKUvI9b!@2JV->$FUj_i1TM`dX{ImWrz_;tkWMSsp{`$wr_D~_g3m=Qg5bCamKZO_Ud)D#@Idxsd-_V8jlph$m@e8fk*fWM7Kdv9^2;FCudZQT4l-$>Qtj+;iEIxPQTPmAH+{HKE_ zHb^n;{7NYN;>|RmF$6?B9+E8o{uS-ue=zn=U7|osnr+&)t(~^D)5cEQwr$(CZQHhO z+g4{i+)-T*r*8j;HAckxB4Wj_Fz$V0udNwgh!M|*D-N-=aIfxZFa)Yq{%foG|uR!U@NMH-S$w$@1`x) zVUuHIV2-eYhC86ugnopjA$G^W;b7F(q^+FO!x%*B+Q|aZTR&A|COvV2Y!n7*?N9>l zkh%fUL3YG+oKb&%e%J@X8~O@1d7Ox}zs3bMb_8v~Cc>}ebcdX{>H;*W(e7j^=mgx$ z4#+8gM39Z<1cZ9?>Y)H>2-Rkq(NG98267UJOvlRpSPr6T39FyV5xt`U4!u<6n$i_+ z!A^-VMtg(Rn^^W6aTtIT#Rx17`)nu*n$?FOxuD;O&je*(u;-@65|;xxcH;W$#n3== z<+nns$(I_5C$NT929v#wF-B{!2F08 z&RZ_C^Xh0SyKiQ$ieqn*8zM$LEt{oB2n$tfQ|(O^EcP=6=03?(eKm~?5Sn;`;|kDI zuS8uPaxZ0O3^&*Skq*X#2BUHqMDim6o*~{)9qrr=B#9DWgtRK)#o+GgEzf+@oJ&YkXL0y}(tO zq6&1ft3X~C0k7JAIBDb6M@Z_6v#l#2CyzNntEW9!-)_@xboHm(mLOG4s*e28>Tnog ztPjPH57I@SxbzlYStqJu`N}#{85gbMuB*wDz@boVZ*%2rx!)QbF?!#Pu>aqZagaUO z7*F^Sb$EgPMzec(*ya7cVf5ibcpLf1XDoy)voHBXIQlDkkWV$5QGL)dRMR46P%oJR z#jJ@U2%+lm@M`{GM4aN%pYrG@e+!+9!{r8GyKT4NfAr-+f0Or7_SW>6+BNzHkpEs5 zL<`U^(FCITHDa@dsCMDm2j$c_Vyd(qzBr7k@^Bc~&wGlvmTJnIh~j*gY9`r53l)0{ z;P8juquN)A?}ZZOQB6w=oA+|DR=Jga}3e~|UnJ8*4m$PtGgdf;O z`=}6zXcq-Zx<6v-HAigr+up2{4-tc6R{DHEb?kDIlbQGfNySK_l>6Xn4vh1*hC_8A zvMeZEO%ck2s0|7!%|+wwc?#4<^0hKH0GM9SXMSBW^z2la1{EIxZ7@@g`*LpGUw z-qcrZq>5z!r6g}4rr(X7`nTVGOw2vKi53SDvW30*O~?-Q6OM?WHXcudNK>}ZinFxY z5n|qHk3iUg`L0BTYi`_+E&?#AYa}iXhmMGa7g1Wbmf$W@t1^rHvH9yOn~nM18IY^1 z>t@H17M^3KPC6iJK4-6@#cCDkf@E1y9-K(s-LoSlf*|&j4f}(KJLJV6TSa?a6og`1 zgZ!9AmLq?*Zqui40?lQm3e22Hx%_#?^j@IFRAsQ3tCYZ6jCX}{WRywe zo|Au*OEh(V%0b4otF5DW)+#ainH`#b5nqhpYkqZ@x>zOERv&<0PS&#jO9AaX?Ci@s z!#KGzKJE!)s~t_OnPeg zvIa4@EHo1i5DY{i%*NW*7arjeZ?Fhh%2>6O9so5uADo2lx5!am?!kV5lf+hbHXcRF zd;|VKi@&3u@2=1)_r~o7JsSi0MX^4jX+Rh29G9^~{SC^o>v_WolT&?j8PuCf>%Bx2 z40%tsO-|zO*Ar_2}r~{#jaCeZ~vopSb#3s3y(3Bg~ zA3jr}NA=%H1Lcm6u-s`#Htn{g(VOqAg!+qw0gA4$xD+%;RUWbXjsH(2ALO`hEONuJ zd)%>KkVK~$W+~ycHBq(W9loeR7K=L z`U;TvZpt>`HmGv%bZ&NOO=b<)wva}tdjWDu`8tlGX$Sb;lyq_IKAeNLF|$A8j-0xX zwTE<2+?(H8Pah9zQ@a$0(P=IpNE9k~wd@j#M5xUU;ix|)rpuwdr)6A_ zxiT&IS2&Xjw~9xAcTlZ2+LFI1MsCY+++dmdJc0)G396T+f=I%$olc@guOgGbdbhgF zKZ|WUs_Oj6{Kw69K3Q-)Vy)vSL}XZs7C(a_7a-Z$P^~S01O6CkZ_^U=3BQX&k(rul~Au65UcZofSDb%eN>h+xz~Y8di@ zRYe79zROS>#4&w2-A<&8J%2%YTq@9=D6)c$yP>b8x7YW9tI-I$E?n)XFYR^r^IwU; zSZ$^W*ms>j=Aav%?s2Av)DBI(I|q-|`xjjDJMvcOCbNmKmh?zg6wn18aouA%c-cXe0Fb7J=7TkjCpVRw7o zDUm~L@1}=CVl(cqrYy8-yG6uKK90Vv(-u#Whe_rby?Uv_B?TRwbIUnTWPOOyPZnFGrTwtHvbwj4Le92zVgkD){-+G>#eLD z^^+h?p{TZF9M`k_#oDQ?1Km!iy~pJR(*8BylTIA22r@D!c#^xB`M3$x$!{szb_#)> z=JYXqm`1bSahlJ0(W$xJOjW`HGH6B;Qbig}@%c*o77s6qq6t?O-;FJMv!@4lXEXYS z_j>(rB3!!L+!TVx)SGgN-n09yM3eTBiP5!R?(4c$vsUh_3njzdMUDPW{ZE6vl}=1; z=&Ws9Jx1%#93RnhE7C&-7*(us%k3hjRWqYcsZ#er@Sp~#Hg>vod-iZoy_h|h+#W-2 zMrkN6Zq@^#M-X+7@vT))S#g+_dB*~b74yGQ@C1X%B<}H6N0UEGX-R;Bn3Z@;8(j9y z%Uid8Q`OKc&p@#3w8-rMXpU(Ph!teEDO25+B#jPOiT}r2H25Ef2BS~ln!)u( z0h`HqpJA@p9ZJF2L`_Sv=iK`hFowI5Vyp_svCXv(fF8Dz3cr5$WAO`-^o1;89-?9n z#SmC>hy35`^S;dq!&Uu?fMj|an&sqKXX5((d*}@Q!35=vU8@d%Z}HpnHBWU>`huBr?iM>aD4o1h&?8>=4m7Kt*hRFN=TS4?xv>Yx?qDE1zt1l6B0RyGhl4 zzJwMMutJ_r%vE$_Uhm~12h}1o=j)pbrA`Ocrm378nvsOo7*qfIJ(-zpKcx{g^@H((Cg3(r| z&eKIxj5qAzq8Ce>yK8zae%%iBUb|%tyrA?k8IfA9td|q;n}kM6wP5G zMb{UWy;;O}b(C_g+3QP>sUdexUvS)O>Rc@@93(J_Jv;_bBw4@-k1Mnlony27_y2@R>TJ)v5$`*gtzMu7cyJ z0GFhc(ZPPPw)m!o%(}EhlJEO-V|V9i2_xDdRq_!<2JQ-y1}WxK@{_RYj2)q>l_`?yebYKl$dh8!a6*x5 zJ^XOyZbLfxr7$lr1LwAmlg+m7q|KGf(Jabj`9~|7)wukiniWn(LRk}o^~&J4cNPfw zCo;X>f0OC8iZ@biT@ilfbBY`;@W~UVj0ZsVj@PNl<_5>W37+kpzqNX!eE61<8qr?g zWeZ?xxpx@rz;xSEfj1>AXG0Fj@A&}MFbF!eBhz33ef9r|I&}&s>Uy;UrXsUoXHTy> zvKsQ`OcysWK(%FUclNwH4p__9)X37=6rV6TJ55bj)9%RF6sx4%I`+xX`566VYIox@ zL00t~!+`=W#XU*TdB0|@%;Um?`AT4qc%#4gQ%%tkz(l)f_p|Z_ z`N%{7(X8oYiUT8TURx%DWA+dnL6-!CBE&PpJ{%s?idA5MU8{jQ+qTQZ`2><{`jPL9VO9#i?-d_NLSj z679fH5bDhAc-Hez$JCHNupMS>(?iNUMjIQ7$!O{}2Ikl#VY=pFhkAtN+Yv)5L)g*m zs%}*t6ZG5U`Mxkxjq-)A)ur?jwaQ46liPIpvP6$7uPOcTtLIGMc`iRv09^i&X%R9L zE05!7k7);1AEnV2B9xp{3|sJ$YgM*9Z>4Z8C?8+(x+=mrGiWd#2L$F=Lf+2n)RLgSZQ3M= zSD*haR5&ZXwao58gP3kLjs||g#oZNoIc|`qe2E69sCWuxEIH5tey9Xdh;~swAXzg8 zEl5^RUri(U+9I^&P_9?CBIHFQ(M(t zOLg}@ts-}6f_!NPy1xCo6(tQ1*tKQm7eZHwy=05Q<`*8PnrOfXMZ89j4$DNeC=p|# znh6Hk?C3Ni@CGe&i<3w{& zgC1Y+A=$2lGc>!@_zAF+da`?O@N$`>gLt?mWM4~LB-|AK^#=@o$6kWt-}9!6$$(Z{ z20-qKvQrc_H`IZ%lFo6f)UOn=gca?J`@nl$@P zPgu00 z#x=D4)nul;WS8{uGk_K<3(-LQJs&eVg(Kukmsi=4r555f0E}jGnK`u^4xsJk;_c@B z%TOS<#IUz;?QDUp21Q?R>Cuwn%lvWvzQNv%G~?x#IebflKlcwX`sqbyjPzFv4?u2| zFDDZ|8z52x%{G%Fwn~8PNZ3GLnz}V`rYjf8E^N@}igt;6j8H9@92u~zq!_cEUJdGj z$DTiZv)t>A@u-4or7h=#{^W1z{P{Ej`TU1S$>iBbrA9}vBrCJ}Qb1qUw|HcF_b@?c z;B!gw?fMi%;a6I12>~{0l4{bhS2dSQ8kvF6U&o~Z$EJ%90w?u`CgTUsu06}tN`63s z^)KN#pOC8%|1Y{X)F~~u-^Z=3fEJI|jI)e&GQYyH6!F}BfXqczt_Iaq7Kf2(YRl(l zbSiv)e<|hC$|s*zvd&8eL=t+!g#=DEGR^>WZj)= zSbxGJHJ7#P8GD>2lM4`AD2Oe#Ug%H1L51HKxk~KAX|$u)b>lr@8Lk2Ny(eNkt&mW3i|@LhFq+Acj#>hF#;7vi8pJ6e0k7XLZg$gWzylbR=TGB~7u{AT~KS zJLa1nhHm4kYhoisAFuUxU?i4wK_mim4piZ}_O7VpfO_)F%vh0rqtPI1KNiIGlBC;M z&^*YDIrv94z@D^QNr^Y;_r>Rv&V}V@rY%KK7U4@3uwpeGX#zZBkOlSb1`{?3b^QL_zAnP6^p&(?@7Xoh%u9+e3R7j~?n;tJ+fF?-ijC&3 z<>nM%gwqfXoUM4yU13X7?ScN-m96YM=jaDi_C^NRjU%tCs*+i5`F{ml;xDu8Fi+lO z1cd_wTj@b1fHzez$<8k>B_<>k2!J%ypnEj6-o@enPP@hZWD5*o-+2B+yWWMP?>y{- zhkk;Bw;_DF+dsyre4g6)oQQsolDE!?Um`&YL{r!j9z(#PL0K@4q}^U9rNNR9&`O{t z-`qrtaCtcqOJ$Lv7BTrrcPyOXn#0w-eyBeOHOPK**13>kHm6~8zq>M;LDNY?(3I;^ zO9>l>TOBWJyxC7GPL9W>h%0SM&oE^3c`~0g`T9m)lf0xb<}`rQaGqNyLeXe@p7Vkm zKiVbq@cg_jbRarcWx=&uJ9APvQ(KNxe2CeL^~TC9yB>-i*>4Bb1+Fg&2Ths zJ{@$I^PNoo%*h+&_~2dOef`i<60X2eOj&Qh2fU&dv2DCZUh~$uw4_XHeJv~?|J~i_ z9oYT9FeCrfpeooLFX;V_(Sd)*=!E~NLABL0u+TFxqBV82vQ$!p1OWaIz3Km_G=3>* zyS*w3*`%HSl7yH! zGlr$>C~<1^?Hhf_r`K}ZX4HDt;iiiOcR1dO5`kGJ5;UU+ujoyN_&dO)j^xycy8=PA zG@hxEN3sb%b`EG7ft4sEhh<(A_qVukX@6FovuxV67dlF{pwSp6mf(5!jx`F65JxyD z7TTd(Xfvypv;aykSK%Eu8f5T6lU-fgFV;~mN7$6QbvlSTw28w^J`qikTt4F;vjJGy zLu_R==Okv62}y{@k~YkyYnKpxpibaEd+`BefQJf0t0y-1r!VqW)f;4C{VY}|;BX8K zwj`st!j_`{1WXUN*Pa602Xr8(s?1X73&6Rn2G}Me*jG(sbDEp>ij%>%BrcVftBMJp z6I*10?cpKg>KIg1o~L(-;U_;5)pncruUgzg`Z256*N6ec!Ho5zX3uq!r3Oxi;XgnY{sCXg_W~cDkgL6v1#_c=Spcx8Ttyju;yg7-iPtPp~~O zL!k`^_=i-#FGhsxiA25?grP)PyX08nrnvt~Iv|ZbJW^^b{|Hnd?>jfyngVOFiM~1_ zcuu`6(X`4>Bh%cKO_?)7L-&0D=+zgVW}}r|SRH z$o(IlZtQkE$ZhGW1hu)ONHVs))X#?( z)mm6ap`39qlbOC*)5lfGmhAcM-x$1EjU+LxvBXTm6mhI1qP~?$>MeCuarENlGTU*} zaFK=xz&#~(M)mkQMGUGnN&UtKb9t`FqdwxWq@?=TbtQ&5DTy0z#;ll`X|pnW@v`!M zA|V~Blse*+vhd|$?Kz}sp>#?(8vB1(z`_9fM-r=wXTJkh>rnPQpS_TRPeeX_6~v9ljJ5CGL{?R;P*}5r(rUwzrq9gL zc|b~ARHN!Z8AI|0E1286JUe|fnO^NzsZr_BYpb;GG4y@)s za6A61W0#+L0U3NZjWQP>768YA#owkP2WmOm(Z&aBkX&wj**$6984DkCM@OcN_&GDC?&2m3V-`JE=*t(9Z=hSl2_wBbu5ogU>i zBUIB~jk+`?=y{fnu8gN8`9 z^k}O(_V!mEaE{5PQ6Wz>dPhIO6UYgr{40Ds60Uwkbt2x(bx92hr|g`YPY1FxG}6l~ z1S!gItLH)qf)y;)iz6X}fN3XU3maV%H=E_iKt@9S zvr~$#ZqQ&xO334Hv6FF}(K>Q0J8^;@EUB7npGVcr&jTkWN?CT>o|LbzfVpp(w<}!> zeM4soEs-pwjcIuL5Y*DYLFiR#v+jhuzW^=Y0RwR=rTxeKOG69&%`7|cW*7wIZvozr z&v1+f#Y??5f#@vxtfOBeq`}cKw7N`6)3P>icWs330tnwwulJZ-F@LmlJ?sn~3N5CR z8#OAM{ya7Ohwho$171xbbWc8R>1qMa>k{nsmZ2l`>X?cyY%GLAA0r+c3GW5T;-uvj zgBum;AeXt*k{@c}fIZox@r znTyN%$O9V8p-LU}=G^}VJR|iC3cl0I9u{*Pixb~RpYwn|dRj;(yfjDpK1UHsMM|LF z9`+w!1Na8?N*R70)=N>W!aTy>Wkm@0ZP_;zWzcZwXlY8PoECjtR~+Ul$&_Kzp1rum zj7P>8YbvPC$*ehzt_L>MMXjo}v|zC8+i^v)LFFw$e{zipjS*L_YA7~2XIwz;8>Vx4 z;~JXCpHrMM#M**iOJDB%zv~C9>=stng2qfxoCx7srS-M(o&6jx-9@?tSFm7LIk%m& zR&3_;W3PhB7=+}QuJms$F4U%##JzVX`$(B3hx7e`O z2XJH>XYEt&Y<%4TR-C=~$ExQ}cvr0riW4%5&AZW6+5v2-(pPUK>WA`dP}tdm!bm06 zMh*J4JyzY8h}kh2H0V=l&BH5wX3m+#sV5?&>X0wRlmJ_Eb`4dz&nr_^%~o`bCkqiZ zO|&}ib8Y#@3HPVzBy38rqWl(_h0~|4J>-ZUD)~1NE|yz7`rjbJyyHW6?6L%g_t8%+ zwGDTK&o$-Gb{Kq=&peKwS;Ksws?k76RZdbSqR)jM>WxZ1_){0=2Ck~rt}92@D&3&( zs^1&PqaW=<18^5{)sVIs9b<@tAHEqD1AzM7-7k4Vud2|c9BFsUM?i0lJ#4#1O9{2g9igA+c5{|z^;I7M91}qkQkuG1A6MU z7(L16R?Silt;$JKsfZE+>{{CJfcR}-q#P8mZS$FO`lL~4FG+naX@Vfh=F<+e(wKnM z94)uML94SHs70Ken9v#P-kF9TD~KU$eqhx+e60vYGKqh*F(yP9oxP~gL*kjxD9Q|t zvw9&musKy5En9vlgt2fsb&UmW(Lhs4e)f&x(UPgzB`kyg4q}MR8|!;|u0TBGr#XFQ zFPzJXCHOzgQi*N$ZITsqobM{Wx{8fka+LLOgAkWtFrJAhQNs*~4o*2B$* zf*0f$J|y)^rnYI(xsShS8Y|JOE9C~Es|_Gi4l=@2TD@kN-6Ka1jcABN>*jZ(J7-Xr zg^i6fPnHh#{(UrB^aquOB};~p5{uO7oxY2;&m{=;Cys zAt;^{(3m0h!P0aos$_eyCE}HMfudbbbW*owA%Dm|Ln8FyU`4l5bX;aUnAur)kig>V zs_e4IkC?baV@3=Ds-mgA^C$_0YI5=wp9Lt$w5_6ei#X+Uz2nSeT4+5)`IrjP$* zSvRyU_0F^Y<$3%yVvlg%W2485jH4u2kb9~pI%+HtrP|k!wFCsfqI-Mj6IHEzO;h~rZA4)`kC=bwds@b z)pOVJcn|se4f#B=(o>@*ZHLKejp=wi9rf(7d`8IvjtPsfzSp3tEd`1{RIXWz&Fty0 zOo}lRlq2maZKZ%RdI|zzIn7YLB{__Q_z<85dg>^d`#)*^+h6^%m5kVv6_VcsyC-xN zlrv;%m)%x&eU}vF9^B~rk$TZY(@2C?`R(mH_u(-`*wiRE&@7%M@M8 z3JrdAw7VgBIzW|YV$3ktjRGBGlszTREo8mpPE3|S%8zT_>+mkrsjgrD#LlHq@Jpex z?!PbK6QU77f7M4e?fR?gjg~Y%8(OrpD68ZN?r&T@p1psXi+0oB6VSiD16cA{2E1AN zAt-KYFM|P<;&fYE#AU?&qu9~EMO;lqqvPQ-?OmGqf?-TOn!w-fk7eYU3Jev~KE+9>>KB&e(AeLnwhd!w@rWFjV3LeUnJ>Z_Qpp z{YG-FUFD4Oe$(`6FafC5{rJl7F~04z&&CQC>x0YA0V0r5)vvY9MK2~{hgC?3@lw`- zhlH=vhw=~;5F0!b<7$+4oE?UwU=2scJnqhs3rh)C?}(jfX$oap@(3uHWKa{3Q`~`| zeY=;JF!0qYuT*U&!vKsQ08phd|=|63GdNoREuJinvL(b z{DP_#tDfuFkg9liuh7!N;uR;s_VPuXY`_Js?Z7K}gxBuV4rd&@swB7oNhPB&X&(t2 z3AIsNSCrkxZAG7uHEg$Ux>XIzHjbYkTaTd8XhaG3iXiX>$td}pb-b~yKo+y@0(cfQ zp@EBx-oJqT*B5uU`$2v&DZ)5RxlA4#;ahG2Y{SpUB3d?r+Ok(B$%x{OZg3p{%tjo( zv#6ImM+OP0L7RLEQXvE1!xYA9$}x#W8w-Y>Oaz&3F?*!QT(dMWZLWhC1_tr-+redO zU?*?c)a~({rtCPa>GsjzoAvS5;fmn3_b!)n%!-_t>6RQ=pQmRDfK4g^yr}c7=tL(tK88E50FK+`$x!?paFF z`J`O!V!UmGZ)xqSx^gv{F9NF7mW8S=_ zv9h6E8o)2p8!Uuf{`ocZK<7)-%EA*!9RaKhr2!S zxZ%IS{%4V;7KOZ{K>`5q<@?_@p4}WQZA|{3H=e6B&K%Z9k$!M~P#Zi~cQK$3sIYaK z2I~wc;ij2fwaQ4ESj2!Q)y2X^7$>@?e!kmJVnx7tUG0>BLbG=sA6d3|1V5Ir+=OY{ zCV=|5;{!?380Vu(q3Hr_Gbha^N8M7v@T6TaVvSvFyJdEXBg8T2k%kS?hGFD4>BaPU zC6HZ@se}Z`84Q@vNcss4ds72%bYMM7ObjQzM7U`e2~xeL6aD~^D$tH{N8XHLoS6&iF2#)6bXCW{g&LCEw&TyP4o=BeHBPR_IT2 zV8sf!2)h0TbY8)^2cY?HJ_n<#unF34bah`phjcXnwzZR$sC>C}>D`pW=S+*Y{G@rF zKJ7F?0GIV~U*1p*qXJ~$kdAYgX&XofgEpdmQWG8DGyKAv`bdh9MacGeB7m8bLWh`W zfxex*^1U$*rhDaf^lwpOsMPzQzYR&3!IL~z%s zGx5K=H3>KIzTEhu9oaPaN@$Z>gxg)dKPF=|6k$WMxc3-Mp{)*>GQWXigNN;kr;^W{ z$2rO6%^+s(y`uRB0znY$ZC>pZ0!V2*?5wdTUDbn85YtxpR8&I>X8 zQ0g6}g@mHa_4Kj2@@9G6%03;O(2k#XCt#xoHb|G3Lp|~^V@GgyJn*lO{TDLMzfZ2L zVck)^z+|>UX#C3aQPeQc7p_ptK~MZQF26JFzmI`E*tNW*#szkcB!D*%+{8?(c&ahI zAt;}=Xk@xY@!75@ykOJmgKt^IV<^X5MlPA1KP=KF-+4FXD?oqIPY@1)?Zb_+#F(Py z5nWmAyr8E4?Snsp+kpJZAbFz$2!Nqijuza3KcUPAtJQ~C2Pxo-j4F$;=7}D_r2cz% zEoIcJaL|BJ2?WzV%aWnH$#!>1w)=vih>W=>Q#RA52Q0JnikIfEPLLTb0KcywuwDN; zZ368d?hj~ixSn7SRF(k0JCi#vRbT54w7H8%IGd)GAJi8RsHxrc^DwaHq*MNqYKdze zYY_@iiwCiDS3Gl_6Xe3g-m&bORZxHn{1#`AKa^0rc7TE3(IpvGXQcx7&m{{*r{UMrnbzn2tR1gj#kfa)tS|06Bm#&!U5&nN8~%UYnvZO#ckU2Y$a_w}?5>{j(;q z5g8*n^wmUL4BYpi3rrP`dJrGn5u{-f7=CwR`Y8`GMBOu0;;06x<+$aF!-XQTx-Ysv zF$$$+^w7CvglP^tGtCHZOG9cexYX@wwh%gyS;DD#g6afFNVAw4Y_5nyAj+huoDuz> zIE*5KcOAQ`j!v0YB@HF`%|-Ku^pl2+Mks1yB=Y(=32OOJGL2NLC&OO`KQHTK-25V z7{;-YY4X7J6Cm4X0ipH>3Grkx(gW4#u8iL?eXxw#IA|4j2}$(s0m_1bj`)+Z7HRr$ zgXEjDP6<{F^FcS94&hn64}4~XE1=`!d^^fe@qS=DNHf(7<*On~F$Kv@;jo-gHAGlY za8f)o`-6hr&_`*}iu8;`u0ZQ?IXhJS|7Ma7n;i8`}wPx-nw9{%x4gfTY`9LsQgD-HEm&! zIXH+b)_Qoi=v={JYtQ)A;n>?Yf8EdG*vIIY5(-0_?hY!-!Yaf!f^iDp7oFKc(^e~l zNzq-;RV}Rip94QoG_}fS?%M0RX>8QJNSFK^=jEFY_9{yNMRK`F$lBy&BAiw=FJdz94juLM8_s+`DFx65NzPUo8dM&GEJAj2M5jHw<=R%V_Z!Fd4IA#Wdw z-Y>6KVKT4RdG01@QUl7L1385GI@dl>TG}48hT4u{x5Cs0N8L!)tyz9)|NfQq7{{vm zTXHPX3?7F9bI{%>y>5UAQ=&(dpjul5q1m!}Jh zlEybi#(^qiVK5IcbDIQ%9hKX_orZtOi*}q7f)pYWRNkzTO2kl8LZZCZXohOy`cOM~ z=)9Z&TukD9@W3nlYiu~rJV7X3Y>@q_S^erz3za9Y>$6h6w3ce2!JL|qG&{I7%#A@w zv$ndR(rb$nuI8qoh*4Ig+&HR#6_UG0j6k8$>{2W z!fN@DGUKRLD8Ud?S)hTd$PwaHfj}ITyC=z8u`G?}J%wg1A#y#1Zd4FkuMl5XMz<=& zw03arTl4I1pND!pg?>~JU#}2%YvbNIS>@%Iu||=Wf{@g}Dj87_rS2wN!m>hFsVs$< zqkhzthow6Ba~+w+)h^J(*4h2bBPo>*RnY7@Ch)I&*ml8s3$H?FQ4(>5?dv zK2^`ij>(w#sAzH3g7@N+P-azKX>nAp_JVhA5!#BJye8@ zvK*jZEFfW}^yBV??A13mE=P(XDmqpCh~AfL$e&J7(au zqPe|ml`bW}MbSOog*-~SeEHaVBb_S3Z&AgNwts|1*JAJ;VYq<0tPM3g^^g`1QFaLv zU9A0n@`q1dw#e8Nhz}L+_s5Q4T6YSc7)3%+w`o&`qMPGs_JpKA9N|dTM#9Z=c2c&@}0q=>0M^l zur{3RvVX2&ZN7V-ydikS%CuY`-MOT^V=?^^<_=5N@7lgqcqu_lrL_+@0sznc$Q|^W zz(y`T(%KG(s~Y_zCaSEgaa1h3+5rbpe7d7N- zGCq10x|K)~g$uvXh>HiV4jD#K!7@c!I$IzX zu?R+HIXj(HPW=tu69_Rf#?)#4YD4WW{U!*(93+C>mV?1@?FXT3?F4^g&EiWgq3Z*K z`upgDIK^q6;TKtpL$v;s7AH~p0W2k(Ny2Om^O8-nybNHqxHJ1GQ?n`X;*3n_S7}}@ zte6@ZXh@6D_VOLwISYeB9YJ)xfS9nGPqa9dsFS*7Aj>disT-5oW&JGMM@ivF$=oxw z6+E8`Rp7r^E!Z-7FM?_BhxT-=lm9si3fLnxHcz2;n^%?zPp*d+~OIHJ@kzYE=8 zGs(-Wg%h}P(DmvL!q(^5vp$x60){RKYFmUh`rROiR3_A#a3*3=Lqa91*u3fel!dRG zjxThLhECzcGruh#u5K(B6`(z%8*Bk4{-~YSKFdD_i$CfZBr@?t^K%%rO(qZgK zv?bhLq!ni_OIlwvdHd}@StaT$s`i(AUD>-ZsZtK_oSbi2P#HoJYYZg93Pl8~v|<5; zb;3xvQpsGTFG9<~Ft{otoy#I({MDB&p6XK(&PJXxa`SWSxAzG&=*Stp! zbdva_niJjqbw7CQNzpO9D8Do&oEV=Kt)I!%OvXRxsofrlAu`$@*63M(}$E6-c&7pnO;p{4Wo@wbGFx>?Dh$8Lm`C{#$)eOkIK`_cc`5b4d~||G)m!S@>>$@<+QYgs-l(>H*Od$D6hsxi1s;SU4CH7_^PK`7oMtCPMu>{ z>wf-U;WzV!i^SVT@4A~OGCnQu3cE2L5gKtPI(F=CPu!*zoC;E@yyHKc{*@L>i&|t2 z-cKYCYO38&qje?JIu(>ty1E44?SOER~0HUAb+ye9acAVVBR@OWd@iRxJwTZ3=Fw<2SC7-9*rk2;(b}Qb&C+kv;nC3W zU?dp77+m@M&p2w@UT7l#4FI4*;eXr1mR6Mee-5LTxUC$p*$lu#o0yUvvSdmY)T$`V5|-?9v?!yi4}QD>AneDy;*z>IBgmqwNFn_JAbbH@ zM`GZHi0?QhjpQfVW+hIOP)dyA`)kV7Pdf_Z_XVuN-SU}~LQfst?MQYjDg!AC<&sn@ zB#06fQQOCjv0-QIf#pdiB*vSl9dV%CajWZRMbm51eBjBsSH3IBB;`|RW;3l zQ;dV0zAITfVQhw^LhD{-e~ks)5k35Jh|idI7pJFftNo`=^{7Gf`U;Zpji0IG|A5PV zSK22V3dM?A7}>yQ4@|)FjXC|kA*+N+gLb4zu)z1>y_D5cc5r^B+gn4BdpH&5Zk8#o zpI8tcEIdB_t_L$4L@7?NkMApF6~5yQRbmqS)Y8Vj*4dkc@L{rpKKzH(K>p|xb){+m z2$3_T!PaP!s$DyP6e%eBj-03|3vsSaoL!mGh@-M8Eqt0iU8sCn+nbr%TiV&OLr+nu znmSop+uGVXeJ#*-?oSP`F%uOHb+P;`siNN0RGNUfX@tT@nX48>WBI=I{yP`{ir0uF z^~{Fs6R3++%i-%OVwQIjxIJKv+s|>;rr*mh%g9!wDd)W4N~DK_z$cGAP|5+Uf!%3JK5{OC8zE~KG#`oFP~V*%gik=pULI>q;#7dm|5b} z)__O79xW6&_wFUN>@%xbx@pBv^NJ%gCDM(HC|Wl6a;mgSuCCAF9R9uW@`WQSc^e5& zAb+(&b6yIZ)i(HHF#5n!oyyS+Q&Ti{uKbO*)Fd&p|#O+OSrC3BSW zwMK#cYa~i(HYFZ3cMbo-BM#MSAV0B1fWj*DFt7rv?vaqy=X}pF<+jyIlT$uwfmmHZ z!)(7~KxiQ2P)o`J#*|VCH!#K7))rC$R|XF=lx-B&+lYfT$-Gd0hIOBGWD0T}8J|us zgQvR)19y^bNj0E+(A;Q(hr;D40Q}Ok?S54H$z2r*PL;`0uHTy*h{0ZMN!#e=<>z>O z5HMU*D9xI_B0}iMc3jh_X+hY`F*|l-pc3F0{kpNLDY6}K9-23K?DXz+fg52UeJ-+X z#I9wL^}f4N2L|BYQHFsTfy|vv-z8A6s$PqDRG>1_>Eae^f>>UL!}Nd>f2jqTj_^c7 z8sHGn+^NuU3xpYA z=Lg?r2#V~Gxv8j4WPC7WnEvFGs{+d)%y-KlB7jFN0$7Eykx7qQupA?CG@BQFY5JE@ zd=ymNmPxLHe&GWm;)|Hohi0GD-tR?mzGe@H6?OdL=%;s~XOArRbAJ$X*jAwxeU;0` z=dQ4qF8-|$1K)OlP(#ET6787mf6TCEIDK==PT82ZuzUJl%eSTN!Tle`&ap?7fLpU| z+qP}nwr$()bK16T+qP}nwsod+Cz;HhzYpI!S|ivjxn71P5G>!(a~6Ojw$ z>%Oenc(@mzNWKw1Zw!P&=Ad0jM8lRB3AtG>upl9?6PdVR z-leA&Fl6vdr@VD8OyOx7Wj|&O*~JjFGZ7xo3-+0$3DPUYD+e<3OwuO}5>MJq1T#58 z;aK`yzDwW)>Ow;L{k%2sQCYA94_z52)yWd!I6@HiuOFIn?8mL>7p#*6yaYsLQd8vP--hw98bmidg9s2tRnEOyn0ffIi;c;Y$5 zmSd=Wfc|6~_~rglOr49(6(7R-OEgt*`#y&V&qZ`TQXClj*r|L`)of!9(BCIyGtZjRYG6%sRFBc!-R<%4Y+*xoR}wnQ>p}Y~ zPVMZnuG10c)ks5jmcg@S@v{D6XUG8qHTJ{=Ps26{LdI|TX-IR4^%L@13~iKKBFk0Y z8gF20YNrumBqE>zdYvk>AuJFRsn!NTBj+#$myRt^(wrC3qC0tBSIZ~=0%Z}Ec4sN6$?4h&T!j3n;|A%#@$f+%9R(2F zYWfDt!mAQjQ<#$R;Lt=5oJY#_HS-PUNF7$H6;SR%(SN!E5(_+y_~Y5!bjBQJFv^Kz zsfR05j-NI?ZR$(6L(SwCYu8#FWnVi=Bp~w1(NxBgp7)Ui!$>gyR}g872Y#I8n&3R` zp1>?o61tIvmubq{@cF{9$&661kw18?63&bnzB%WcoA8t1%wrs^&u;RTs+?UFu?hBO zKlCN1S%%hos9-*b_AE^uS{Q!ta?Y*Itxj*J&Z48bS!cBFGNUve6}>XClxchKug4h4 z6L2e5oadyUqb49RztAZnX#8AGtPKBA#VzaWx=02k9Xl7(U^DF8Flj81>Mig+o4)l) zyvLT~LN#kc+1_|wq*x$Gpbw20;|B>3dkohFusEN;we>5|ASxh(a6{Sv_bN2_0R@2E zKhS=)zvL?dXiRuhqp1FeZ>3qhsS$gCorrC_Y5(?X+?3+}0%VpoYo%QMyj2MZUfG5V z`Hp2qk<|3|_SQK%Xg!(?bP&GsiFV!)@b}v~jr$3S6k#ql#J->#{9bKsI4oTYsj6K@ zRfv8(H=2Jl;Y=Xm!h;hiXRc1-&jwZ6&Sk!qI*;s*K|My~;~y8~oG6Gg<|lPKugPGr z3{9ETo1mu!C}7|LL8k^O3d*pCyO1uvLXc{|Wl!H1unA<$Q3LFk>GrD2bBOw(4FiZ= zXx#q;m-W>9qn1@RK%%HP&k2Op0fH6zhhWxTJ);uwssx=-t za*RKMUAY6Ww%bhZTnungXBPV)G)tSmiuak8%kV5b@B*N8sHLTZd7iPuv%tnIy@bYj!SonVJ>cx|*?51EW@2IS!$)3<8|FC|H7JEqh^z2KAA_ zjDAn_{Xq$vu$*x~B7GJh3@t(lOo$wnCNqd?+#0lz6YhT?Hrwhj6xwdX3V9?#v0OT{XNW3o@-@U z?W8TNMx-J_v}NlL%TP4h$@O&ruUy)7)K0!nu4nC%Ll)Rzxqxb&t?Cmcn> zx5v|^a2jYCCfRi~y8CV3ALUuZ_-}{1oI*kC1LWrx2}5p0gqA`VC74uD!|XJ?>3%H@ zhN`OWJ1nlbMnQUikJxav6$Ot{l9$T2G~itvr5xb1^hva`F>DMk-|MClf4Ey?NoNdl z`3<9>EQ=EYzdlPJ8KncHqxd^N7CkIdTT z&Tu3PieV?-#)n%#d~x!jsn6YGBbyYbX(SZl*AFkyAW!QAx(Ui!8Q1gBInHY|lt)Sd zRkN{*vKzura%#)U3mc|=Ji5GaX2)t0z9v645-khgJiF1(b*mgE<&m{ zJ6prUBUnuRq}p-)1qS1S19;z^M}#|ISPK4;H*1#BV45$`l4We_GXgA4ufV|?+!~6@0H?8J zxHMTf#4Q}|E)bqE4!WC$V}r$$b*aDeJwaHj7KwQA_I$kjZol}*-m=&YX^hi02=@<8 z(>Gs0D^$al@!Y%(6oZQBqB1#OdFULLKW2axtqqYPJT@M0MY5V$_&OUbKz75eXN>LB zZ3z$1$Jd_OCGGm5rA9Yem0B_OI)DT8g&*V3+*w%CQ1R@OY zx$ZpO8U}Zu?8A_mk63pT)IKaE#w;_K+hC4+Rm^}-;&X=EY0683w(ZT_2ugN#IMv|Hsqq5&N`qL@=^Qi5R@W{cVJ^!tN=p^6Pw{NYkH8&VHyGlL$t+D^h z=b$jQtGlb8k?sej+-F_X5BJ?Z?Z^lJKH&n3Awm=)i0DmOhm2p0NxUy|4mW6e#mgJh^WUK+<)s zWXT13!zVyGg7CirFR_-SH^@0q|h zIZ^Hq{{fnM7Q-XhFH|R?sx6q*wO@(mEHMS)g{G)gX1%&t#kHgnUgeT3wS)>C6z8_b zfszI{H!*hIs}YYbAB<8`HHTOZ!->s6OLT&NeeYJGoH^K=k%|!?8-OZzQfrg*PHqiH znp2h)O^dA-)Z?NiA${I$r$lAFE*S3Kw-i6^-(=}5#u*&gZ^AVUH|Q(ObEF57|98cXRhiEWe2P*VX4e76W31nd^&v72YMX@W7yKRl}!R> z-;nN7fgy9L_^$$cpjApd?K*MCCr~zQT-6J(;Cx@$ z-qqCRkd8`1)L?w-jPG`8Gh6fN}8_#V30BOh0b<*W%=8E6ZQn9Zg=r!S23k# zo-{2$*n}WsLE1!xeCDrriIm`iIs<-5yE0?vUq|ma20{lO2`yjs#h;dg%L2oKIymH0 z!OvD!uG3;Z^7h5oMVwIKYsY_JZy_ zR_bOR2Oo8ISOl50i=V}|WSA4dxW(PsUShETUvNWU=&?%Q&FpSPr0Ia6 z+a)CK|3&g>`cZAlycg}KHxD}ZZY?-Y?aUYc>L(r%EjhU6H=Z?gG>SoNb7`#i*B7oA zavqN}_kGV4T$px45Ak;q2ovn2p9tRF6wYvCM9%oW_@Xy++?N@;zzZ8gYL|{=(60x% zc--FEGk7hVULitbEv6y3rbG#uUm9Rrc=BgEESi+gkC0D`O63gX4dBcxfM`^!GAxT~R zZHiTB-oJ#EiM_G29=v!g#&=aF^wq90Ex<5vk8QR;c%s_sHA@$x1&;mMNCtyz)HN3X zNn4;Ud!>)aGT3Brr=;fVqqV^ftkF|C8hRT)Bb$!CjoeArYLfwesiDMLBiz%AJd&WP zR#fF`OFDE`QuRuU?4Go2d*JUZ_+#5vWR?%u8}x4_Bzx-tswXrp#GPnPP@aKDqTU*2 zCbYfsX447?KXWG~0Q5AsMQ8WX^6u)`c#&snLf6`s<6|`C(>(QpCPqR8meFwf{_At_-~V%Qna+n-X9o`e0LumdK>DAWoWHe(|M7-mv$_W>OQ8A)FcD7b5Fh5Pa9;s0x zQ?{=s{DBm3A~hZ@lcf{Q`AtqEIX0_a_heWOMFlxT;fN=fQG z7XgF%(D+etp(u7+6sk{;JoIG9Xg#Wk{1*gA(6N0#GSG4{EAD5Dvj>e`y*H`7Xu?Us z+yrnnU$9knpvqjc)@Zzp(6OW1!5^L-ZGv-_R1Ny{U)1rpsnAJNe9yae`srnoLMCC=)pWD2zL%lZ;Q*~>n4esmarZ_`Y&Hr-ac3-uuefv(2j<^NO%SL5 z34%xSDX8VY__C`17hm>Ve#l}0arQHDzDQhPC%Q&2x3?SV_9Hd*`+nJi1MxJ~$brv~ zu>&hR_Wr#1EOs~YR{Rj1H=lrYPce4|(-DA)IH4?~-Yc7x6KPECtWmF$Aw!%{LmDku zhoWA>UL_^2yh^ntg2hrTun+TZc%f=iqvW}655)X@KOl&&>>mOv@@Ed+uftz_?AO~K zfJ(7u&u2@o9(4dmyITFk;*rPgL~(>*2Hea7=+Cu&>8c{bR09ik5wq(nQ=jIa<9*ny z+JgJSuK3)5SZds^cS9x<x90y}-BHC2BJSv_nXu{ZytA7)SM|4O8Cf5& z1);}(`0P>?+lm-c&@~SRa?EN=rl~pv$pqt)yW;ya5~@3>`3NNoz<1xtiXD1@nyHiN z7t>E1jKc}5z%s}?kuoq4>1JT_h-sOQfHA<*xD_kS@e?&ss-&NDarQ9_C@XKo#2CRX zGgB~y{~0YIE5;-tjj}zLX&GfDif)(y+aYgU|CW$AnMQ2$f8Ls( zn_3P;rLFB9K|dY#8ww;iAYqV_WmD3mBbLyBa*&vwF$Vq+)TU2R=9AVHPXtv>GJjv) zgrWYbKWLlPZ<`^PBbsQgpq!L840u%KLaZt zCtrEz^NZdYHNg=zfra80*yJ{=*^FE__Vsjf{&9FQc75HwJizn;K!~n0EpvLlEZXPz zd|u~7Yy8{)t*PPD(S_2B^TU$IBv1|+J`OMHF3{A`yV4Sjid|}f0QmJYMia#rt_0(( zknuKIa~#yAR3(d37{VYJUo??ZBT-eYIpC+KqHwrZ6%8xxDc>%kfI=gMor<%<_!kM< z&@cjq9=V&lYWl@<9{Go|4p9aWk3XLC=kmOturClqzGh7tO$^LzHif&NKaoj6N4ME5 z;zc~X<4lNj39e8Z9Kr

UKmIas$A(z~a^EA#X+ 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⊀`RWm9GuT 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%0Dcr!jwAda(Wo?xx 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 diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..3a5356e64 --- /dev/null +++ b/pom.xml @@ -0,0 +1,166 @@ + + 4.0.0 + ch.eitchnet + ch.eitchnet.utils + jar + 1.0-SNAPSHOT + ch.eitchnet.utils + https://github.com/eitch/ch.eitchnet.utils + + + UTF-8 + + + + + 2011 + + + GNU Lesser General Public License + http://www.gnu.org/licenses/lgpl.html + repo + + + + eitchnet.ch + http://blog.eitchnet.ch + + + + eitch + Robert von Vurg + eitch@eitchnet.ch + http://blog.eitchnet.ch + eitchnet.ch + http://blog.eitchnet.ch + + architect + developer + + +1 + + http://localhost + + + + + + Github Issues + https://github.com/eitch/ch.eitchnet.utils/issues + + + + + + scm:git:https://github.com/eitch/ch.eitchnet.utils.git + scm:git:git@github.com:eitch/ch.eitchnet.utils.git + https://github.com/eitch/ch.eitchnet.utils + + + + + + + junit + junit + 4.10 + test + + + log4j + log4j + 1.2.17 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.6 + 1.6 + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + true + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 2.3 + + UTF-8 + + + + + + diff --git a/src/ch/eitchnet/rmi/RMIFileClient.java b/src/main/java/ch/eitchnet/rmi/RMIFileClient.java similarity index 100% rename from src/ch/eitchnet/rmi/RMIFileClient.java rename to src/main/java/ch/eitchnet/rmi/RMIFileClient.java diff --git a/src/ch/eitchnet/rmi/RmiFileDeletion.java b/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java similarity index 100% rename from src/ch/eitchnet/rmi/RmiFileDeletion.java rename to src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java diff --git a/src/ch/eitchnet/rmi/RmiFileHandler.java b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java similarity index 100% rename from src/ch/eitchnet/rmi/RmiFileHandler.java rename to src/main/java/ch/eitchnet/rmi/RmiFileHandler.java diff --git a/src/ch/eitchnet/rmi/RmiFilePart.java b/src/main/java/ch/eitchnet/rmi/RmiFilePart.java similarity index 100% rename from src/ch/eitchnet/rmi/RmiFilePart.java rename to src/main/java/ch/eitchnet/rmi/RmiFilePart.java diff --git a/src/ch/eitchnet/rmi/RmiHelper.java b/src/main/java/ch/eitchnet/rmi/RmiHelper.java similarity index 100% rename from src/ch/eitchnet/rmi/RmiHelper.java rename to src/main/java/ch/eitchnet/rmi/RmiHelper.java diff --git a/src/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java similarity index 100% rename from src/ch/eitchnet/utils/helper/FileHelper.java rename to src/main/java/ch/eitchnet/utils/helper/FileHelper.java diff --git a/src/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java similarity index 100% rename from src/ch/eitchnet/utils/helper/Log4jConfigurator.java rename to src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java diff --git a/src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java similarity index 100% rename from src/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java rename to src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java diff --git a/src/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java similarity index 100% rename from src/ch/eitchnet/utils/helper/StringHelper.java rename to src/main/java/ch/eitchnet/utils/helper/StringHelper.java diff --git a/src/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java similarity index 100% rename from src/ch/eitchnet/utils/helper/SystemHelper.java rename to src/main/java/ch/eitchnet/utils/helper/SystemHelper.java diff --git a/src/ch/eitchnet/utils/objectfilter/ITransactionObject.java b/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java similarity index 100% rename from src/ch/eitchnet/utils/objectfilter/ITransactionObject.java rename to src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java similarity index 100% rename from src/ch/eitchnet/utils/objectfilter/ObjectCache.java rename to src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java diff --git a/src/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java similarity index 100% rename from src/ch/eitchnet/utils/objectfilter/ObjectFilter.java rename to src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java diff --git a/src/ch/eitchnet/utils/objectfilter/Operation.java b/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java similarity index 100% rename from src/ch/eitchnet/utils/objectfilter/Operation.java rename to src/main/java/ch/eitchnet/utils/objectfilter/Operation.java From c65fe9c247f5d6e6b847cb745557f743a2c184e5 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 28 Jul 2012 15:44:47 +0200 Subject: [PATCH 005/180] [Minor] added eclipse settings path to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 2595e2a6b..76c4111c6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ tmp/ # Maven target target/ +# Eclipse settings +.classpath +.project +.settings/ From 7dbd5af6a71ceb84cf8b0b13f5a8599565efede5 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 28 Jul 2012 22:50:52 +0200 Subject: [PATCH 006/180] [Bugfix] fixed a resource leak where a BufferedReader was not close In FileHelper.readFileToString() the BufferedReader was not closed. This has been corrected --- .../java/ch/eitchnet/utils/helper/FileHelper.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 4f310ec8a..66fdc5369 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -60,9 +60,11 @@ public class FileHelper { * @return the contents of a file as a string */ public static final String readFileToString(File file) { + + BufferedReader bufferedReader = null; try { - BufferedReader bufferedReader = new BufferedReader(new FileReader(file)); + bufferedReader = new BufferedReader(new FileReader(file)); StringBuilder sb = new StringBuilder(); String line; @@ -77,6 +79,14 @@ public class FileHelper { throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); } catch (IOException e) { throw new RuntimeException("Could not read file " + file.getAbsolutePath()); + } finally { + if (bufferedReader != null) { + try { + bufferedReader.close(); + } catch (IOException e) { + logger.error("Failed to close BufferedReader: " + e.getLocalizedMessage()); + } + } } } From ffc9fa77cdf7f9d03b33406104091fec7754095e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 28 Jul 2012 23:16:16 +0200 Subject: [PATCH 007/180] [Minor] change mvn eclipse configuration to load sources as well --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index 3a5356e64..7ad55e0a2 100644 --- a/pom.xml +++ b/pom.xml @@ -126,6 +126,16 @@ + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + true + + org.apache.maven.plugins From 2afba1876c6c60305029197b2689f9e8c5a6a53b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 29 Jul 2012 18:27:15 +0200 Subject: [PATCH 008/180] [Major] added methods to SystemHelper and new class ProcessHelper SystemHelper now has a number of methods to query OS information ProcessHelper is a helper to execute system calls --- build.xml | 73 ------- pom.xml | 17 +- .../java/ch/eitchnet/rmi/RMIFileClient.java | 11 +- .../java/ch/eitchnet/rmi/RmiFileDeletion.java | 11 +- .../java/ch/eitchnet/rmi/RmiFileHandler.java | 11 +- .../java/ch/eitchnet/rmi/RmiFilePart.java | 11 +- src/main/java/ch/eitchnet/rmi/RmiHelper.java | 11 +- .../ch/eitchnet/utils/helper/FileHelper.java | 11 +- .../utils/helper/Log4jConfigurator.java | 158 ++++++++++++-- .../utils/helper/Log4jPropertyWatchDog.java | 11 +- .../eitchnet/utils/helper/ProcessHelper.java | 200 ++++++++++++++++++ .../eitchnet/utils/helper/StringHelper.java | 11 +- .../eitchnet/utils/helper/SystemHelper.java | 100 ++++++++- .../objectfilter/ITransactionObject.java | 11 +- .../utils/objectfilter/ObjectCache.java | 11 +- .../utils/objectfilter/ObjectFilter.java | 11 +- .../utils/objectfilter/Operation.java | 11 +- 17 files changed, 481 insertions(+), 199 deletions(-) delete mode 100644 build.xml create mode 100644 src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java diff --git a/build.xml b/build.xml deleted file mode 100644 index ee31a36fb..000000000 --- a/build.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Cleaning build folder ${buildFolder} - - - - - - - Cleaning dist folder ${distFolder} - - - - - - - diff --git a/pom.xml b/pom.xml index 7ad55e0a2..ffcf01466 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ch.eitchnet ch.eitchnet.utils jar - 1.0-SNAPSHOT + 0.1.0-SNAPSHOT ch.eitchnet.utils https://github.com/eitch/ch.eitchnet.utils @@ -146,6 +146,21 @@ 1.6 + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + attach-sources + verify + + jar-no-fork + + + + org.apache.maven.plugins diff --git a/src/main/java/ch/eitchnet/rmi/RMIFileClient.java b/src/main/java/ch/eitchnet/rmi/RMIFileClient.java index d5ad7c2cd..9ddbab772 100644 --- a/src/main/java/ch/eitchnet/rmi/RMIFileClient.java +++ b/src/main/java/ch/eitchnet/rmi/RMIFileClient.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java b/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java index 0c68caba3..a8fc6d385 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java index 47aba008b..f21355aae 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/main/java/ch/eitchnet/rmi/RmiFilePart.java b/src/main/java/ch/eitchnet/rmi/RmiFilePart.java index 8bddfed1c..750b993a6 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFilePart.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFilePart.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/main/java/ch/eitchnet/rmi/RmiHelper.java b/src/main/java/ch/eitchnet/rmi/RmiHelper.java index 96aa27879..a8530ad0c 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiHelper.java +++ b/src/main/java/ch/eitchnet/rmi/RmiHelper.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.rmi; diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 66fdc5369..9cba18dc3 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.helper; diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java index e70672640..327ab5446 100644 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java +++ b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.helper; @@ -28,6 +23,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.Date; import java.util.Properties; @@ -96,37 +92,80 @@ public class Log4jConfigurator { *

* Any properties in the properties are substituted using * {@link StringHelper#replaceProperties(Properties, Properties)} and then the configuration file is written to a - * new file /tmp/{@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally + * new file /tmp/ {@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally * {@link PropertyConfigurator#configureAndWatch(String)} is called so that the configuration is loaded and log4j * watches the temporary file for configuration changes *

*/ public static synchronized void loadLog4jConfiguration() { - // first clean up any old watch dog in case of a programmatic re-load of hte configuration - cleanupOldWatchdog(); - - // get a configured log4j properties file, or use default RSPConfigConstants.FILE_LOG4J + // get a configured log4j properties file, or use default + // RSPConfigConstants.FILE_LOG4J String fileLog4j = SystemHelper.getProperty(Log4jConfigurator.class.getName(), Log4jConfigurator.PROP_FILE_LOG4J, Log4jConfigurator.FILE_LOG4J); // get the root directory String userDir = System.getProperty("user.dir"); String configDir = userDir + "/config/"; - String tmpDir = userDir + "/tmp/"; - // load the log4j.properties file String pathNameToLog4j = configDir + fileLog4j; File log4JPath = new File(pathNameToLog4j); - if (!log4JPath.exists()) - throw new RuntimeException("The log4j configuration file does not exist at " + log4JPath.getAbsolutePath()); + + try { + + // load the log4j.properties file + if (!log4JPath.exists()) + throw new RuntimeException("The log4j configuration file does not exist at " + + log4JPath.getAbsolutePath()); + + // now perform the loading + loadLog4jConfiguration(log4JPath); + + } catch (Exception e) { + + Log4jConfigurator.configure(); + logger.error(e, e); + logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file exists at " + + log4JPath.getAbsolutePath()); + + } + } + + /** + *

+ * Loads the given log4j configuration + *

+ * + *

+ * Any properties in the properties are substituted using + * {@link StringHelper#replaceProperties(Properties, Properties)} and then the configuration file is written to a + * new file /tmp/ {@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally + * {@link PropertyConfigurator#configureAndWatch(String)} is called so that the configuration is loaded and log4j + * watches the temporary file for configuration changes + *

+ * + * @param log4jConfigPath + */ + public static synchronized void loadLog4jConfiguration(File log4jConfigPath) { + + if (log4jConfigPath == null) + throw new RuntimeException("log4jConfigPath may not be null!"); + + // first clean up any old watch dog in case of a programmatic re-load of the configuration + cleanupOldWatchdog(); + + // get the root directory + String userDir = System.getProperty("user.dir"); + String tmpDir = userDir + "/tmp/"; String pathNameToLog4jTemp = tmpDir + Log4jConfigurator.FILE_LOG4J_TEMP; Properties log4jProperties = new Properties(); FileInputStream fin = null; FileOutputStream fout = null; + try { - fin = new FileInputStream(pathNameToLog4j); + + fin = new FileInputStream(log4jConfigPath); log4jProperties.load(fin); fin.close(); @@ -142,7 +181,8 @@ public class Log4jConfigurator { log4jProperties.store(fout, "Running instance log4j configuration " + new Date()); fout.close(); - // XXX if the server is in a web context, then we may not use the FileWatchDog + // XXX if the server is in a web context, then we may not use the + // FileWatchDog BasicConfigurator.resetConfiguration(); watchDog = new Log4jPropertyWatchDog(pathNameToLog4jTemp); watchDog.start(); @@ -150,10 +190,12 @@ public class Log4jConfigurator { logger.info("Log4j is configured to use and watch file " + pathNameToLog4jTemp); } catch (Exception e) { + Log4jConfigurator.configure(); logger.error(e, e); - logger.error("Log4j COULD NOT BE INITIALIZED. Please check the " + fileLog4j + " file at " - + pathNameToLog4j); + logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file at " + + log4jConfigPath); + } finally { if (fin != null) { try { @@ -172,6 +214,80 @@ public class Log4jConfigurator { } } + /** + *

+ * Loads the log4j configuration file as a class resource by calling {@link Class#getResourceAsStream(String)} for + * the given class + *

+ * + * @param clazz + */ + public static synchronized void loadLog4jConfigurationAsResource(Class clazz) { + + try { + + if (clazz == null) + throw new RuntimeException("clazz may not be null!"); + + InputStream resourceAsStream = clazz.getResourceAsStream("/" + FILE_LOG4J); + if (resourceAsStream == null) { + throw new RuntimeException("The resource '" + FILE_LOG4J + "' could not be found for class " + + clazz.getName()); + } + + // load the properties from the input stream + Properties log4jProperties = new Properties(); + log4jProperties.load(resourceAsStream); + + // and then + loadLog4jConfiguration(log4jProperties); + + } catch (Exception e) { + Log4jConfigurator.configure(); + logger.error(e, e); + logger.error("Log4j COULD NOT BE INITIALIZED. Please check that the log4j configuration file '" + + FILE_LOG4J + "' exists as a resource for class " + clazz.getName() + + " and is a valid properties configuration"); + } + } + + /** + *

+ * Loads the given log4j configuration. Log4j is configured with the given properties. The only change is that + * {@link StringHelper#replaceProperties(Properties, Properties)} is used to replace any properties + *

+ * + *

+ * No property watch dog is loaded + *

+ * + * @param log4jProperties + * the properties to use for the log4j configuration + */ + public static synchronized void loadLog4jConfiguration(Properties log4jProperties) { + + try { + + if (log4jProperties == null) + throw new RuntimeException("log4jProperties may not be null!"); + + // first clean up any old watch dog in case of a programmatic re-load of the configuration + cleanupOldWatchdog(); + + // replace any variables + StringHelper.replaceProperties(log4jProperties, System.getProperties()); + + // now configure log4j + PropertyConfigurator.configure(log4jProperties); + logger.info("Log4j is configured using the given properties."); + + } catch (Exception e) { + Log4jConfigurator.configure(); + logger.error(e, e); + logger.error("Log4j COULD NOT BE INITIALIZED. The given log4jProperties seem not to be valid!"); + } + } + /** * Cleanup a running watch dog */ diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java index a41f8dc2a..a4f2aecda 100644 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java +++ b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.helper; diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java new file mode 100644 index 000000000..30a7a17bd --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2012 + * + * This file is part of ch.eitchnet.java.utils + * + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ch.eitchnet.java.utils. If not, see . + * + */ +package ch.eitchnet.utils.helper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.apache.log4j.Logger; + +/** + * @author Robert von Burg + */ +public class ProcessHelper { + + private static final Logger logger = Logger.getLogger(ProcessHelper.class); + + public static ProcessResult runCommand(String command) { + final StringBuffer sb = new StringBuffer(); + sb.append("=====================================\n"); + try { + + final Process process = Runtime.getRuntime().exec(command); + + final BufferedReader errorStream = new BufferedReader( + new InputStreamReader(process.getErrorStream())); + Thread errorIn = new Thread("errorIn") { + @Override + public void run() { + readStream(sb, "[ERROR] ", errorStream); + } + }; + errorIn.start(); + + final BufferedReader inputStream = new BufferedReader( + new InputStreamReader(process.getInputStream())); + Thread infoIn = new Thread("infoIn") { + @Override + public void run() { + readStream(sb, "[INFO] ", inputStream); + } + }; + infoIn.start(); + + int returnValue = process.waitFor(); + + errorIn.join(100l); + infoIn.join(100l); + sb.append("=====================================\n"); + + return new ProcessResult(returnValue, sb.toString(), null); + + } catch (IOException e) { + throw new RuntimeException("Failed to perform command: " + + e.getLocalizedMessage(), e); + } catch (InterruptedException e) { + logger.error("Interrupted!"); + sb.append("[FATAL] Interrupted"); + return new ProcessResult(-1, sb.toString(), e); + } + } + + public static ProcessResult runCommand(File workingDirectory, + String... commandAndArgs) { + + if (!workingDirectory.exists()) + throw new RuntimeException("Working directory does not exist at " + + workingDirectory.getAbsolutePath()); + if (commandAndArgs == null || commandAndArgs.length == 0) + throw new RuntimeException("No command passed!"); + + final StringBuffer sb = new StringBuffer(); + sb.append("=====================================\n"); + try { + + ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs); + processBuilder.environment(); + processBuilder.directory(workingDirectory); + + final Process process = processBuilder.start(); + + final BufferedReader errorStream = new BufferedReader( + new InputStreamReader(process.getErrorStream())); + Thread errorIn = new Thread("errorIn") { + @Override + public void run() { + readStream(sb, "[ERROR] ", errorStream); + } + }; + errorIn.start(); + + final BufferedReader inputStream = new BufferedReader( + new InputStreamReader(process.getInputStream())); + Thread infoIn = new Thread("infoIn") { + @Override + public void run() { + readStream(sb, "[INFO] ", inputStream); + } + }; + infoIn.start(); + + int returnValue = process.waitFor(); + + errorIn.join(100l); + infoIn.join(100l); + sb.append("=====================================\n"); + + return new ProcessResult(returnValue, sb.toString(), null); + + } catch (IOException e) { + throw new RuntimeException("Failed to perform command: " + + e.getLocalizedMessage(), e); + } catch (InterruptedException e) { + logger.error("Interrupted!"); + sb.append("[FATAL] Interrupted"); + return new ProcessResult(-1, sb.toString(), e); + } + } + + public static class ProcessResult { + public final int returnValue; + public final String processOutput; + public final Throwable t; + + public ProcessResult(int returnValue, String processOutput, Throwable t) { + this.returnValue = returnValue; + this.processOutput = processOutput; + this.t = t; + } + } + + private static void readStream(StringBuffer sb, String prefix, + BufferedReader bufferedReader) { + String line; + try { + while ((line = bufferedReader.readLine()) != null) { + sb.append(prefix + line + "\n"); + } + } catch (IOException e) { + String msg = "Faild to read from " + prefix + " stream: " + + e.getLocalizedMessage(); + sb.append("[FATAL] " + msg + "\n"); + } + } + + public static void openFile(File pdfPath) { + + ProcessResult processResult; + if (SystemHelper.isLinux()) { + processResult = runCommand("xdg-open " + pdfPath.getAbsolutePath()); + } else if (SystemHelper.isMacOS()) { + processResult = runCommand("open " + pdfPath.getAbsolutePath()); + } 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); + } else { + throw new UnsupportedOperationException("Unexpected OS: " + + SystemHelper.osName); + } + + logProcessResult(processResult); + } + + public static void logProcessResult(ProcessResult processResult) { + if (processResult.returnValue == 0) { + logger.info("Process executed successfully"); + } else if (processResult.returnValue == -1) { + logger.error("Process execution failed:\n" + + processResult.processOutput); + logger.error(processResult.t, processResult.t); + } else { + logger.info("Process execution was not successful with return value:" + + processResult.returnValue + + "\n" + + processResult.processOutput); + } + } +} diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index c5807b2c1..c69de70d0 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.helper; diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index 2449d9038..eeb3aa284 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -1,29 +1,25 @@ /* * Copyright (c) 2012 * - * Robert von Burg - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.helper; + /** * A helper class for {@link System} methods * @@ -31,6 +27,94 @@ package ch.eitchnet.utils.helper; */ public class SystemHelper { + private static final SystemHelper instance; + + static { + instance = new SystemHelper(); + } + + public static SystemHelper getInstance() { + return instance; + } + + public static final String osName = System.getProperty("os.name"); + public static final String osArch = System.getProperty("os.arch"); + public static final String osVersion = System.getProperty("os.version"); + public static final String javaVendor = System.getProperty("java.vendor"); + public static final String javaVersion = System.getProperty("java.version"); + + /** + * private constructor + */ + private SystemHelper() { + // + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return asString(); + } + + /** + * @see java.lang.Object#toString() + */ + public static String asString() { + StringBuilder sb = new StringBuilder(); + sb.append(osName); + sb.append(" "); + sb.append(osArch); + sb.append(" "); + sb.append(osVersion); + sb.append(" "); + sb.append("on Java " + javaVendor); + sb.append(" version "); + sb.append(javaVersion); + return sb.toString(); + } + + public static String getUserDir() { + return System.getProperty("user.dir"); + } + + public static boolean isMacOS() { + return osName.startsWith("Mac"); + } + + public static boolean isWindows() { + return osName.startsWith("Win"); + } + + public static boolean isLinux() { + return osName.startsWith("Lin"); + } + + public static boolean is32bit() { + return osArch.equals("x86") || osArch.equals("i386") || osArch.equals("i686"); + } + + public static boolean is64bit() { + return osArch.equals("x86_64") || osArch.equals("amd64"); + } + + 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() { + return "Memory available " + getMaxMemory() + " / Used: " + getUsedMemory() + " / Free:" + getFreeMemory(); + } + /** * 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 diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java b/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java index a243ed9ba..c9bfc3efe 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Michael Gatto - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.objectfilter; diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index b329c4f0b..e2d0b5e09 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Michael Gatto - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.objectfilter; diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index 4a29eec99..a79970c11 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Michael Gatto - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.objectfilter; diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java b/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java index 8d23723ba..1dce6362a 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java @@ -1,25 +1,20 @@ /* * Copyright (c) 2012 * - * Michael Gatto - * - */ - -/* * This file is part of ch.eitchnet.java.utils * - * Privilege is free software: you can redistribute it and/or modify + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Privilege is distributed in the hope that it will be useful, + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License - * along with Privilege. If not, see . + * along with ch.eitchnet.java.utils. If not, see . * */ package ch.eitchnet.utils.objectfilter; From 0d392b1c602acc10b97d2f863b8d1f7e0f415c31 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Nov 2012 09:58:52 +0100 Subject: [PATCH 009/180] [New] added new formatting functions in StringHelper - formatNanoDuration(long:nanos):String - formatMillisecondsDuration(millis:long):String --- pom.xml | 1 + .../eitchnet/utils/helper/StringHelper.java | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pom.xml b/pom.xml index ffcf01466..20471eb4a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,7 @@ 4.0.0 + ch.eitchnet ch.eitchnet.utils jar diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index c69de70d0..5911a4693 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -413,4 +413,40 @@ public class StringHelper { return sb.toString(); } + + /** + * Formats the given number of milliseconds to a time like 0.000s/ms + * + * @param millis + * the number of milliseconds + * + * @return format the given number of milliseconds to a time like 0.000s/ms + */ + public static String formatMillisecondsDuration(final long millis) { + if (millis > 1000) { + return String.format("%.3fs", (((double) millis) / 1000)); //$NON-NLS-1$ + } + + return millis + "ms"; //$NON-NLS-1$ + } + + /** + * Formats the given number of nanoseconds to a time like 0.000s/ms/us/ns + * + * @param nanos + * the number of nanoseconds + * + * @return format the given number of nanoseconds to a time like 0.000s/ms/us/ns + */ + public static String formatNanoDuration(final long nanos) { + if (nanos > 1000000000) { + return String.format("%.3fs", (((double) nanos) / 1000000000)); //$NON-NLS-1$ + } else if (nanos > 1000000) { + return String.format("%.3fms", (((double) nanos) / 1000000)); //$NON-NLS-1$ + } else if (nanos > 1000) { + return String.format("%.3fus", (((double) nanos) / 1000)); //$NON-NLS-1$ + } else { + return nanos + "ns"; //$NON-NLS-1$ + } + } } From bb7042524f08541309562a37078902cdde53486f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Nov 2012 19:42:58 +0100 Subject: [PATCH 010/180] [New] added new utility method StringHelper.isEmpty() which also checks for null --- .../java/ch/eitchnet/utils/helper/StringHelper.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 5911a4693..c44a2ac5e 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -449,4 +449,16 @@ public class StringHelper { return nanos + "ns"; //$NON-NLS-1$ } } + + /** + * 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(); + } } From 2fc807fe11a9a29c9d1c4954fefb20341086f649 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 19 Nov 2012 22:44:07 +0100 Subject: [PATCH 011/180] [Minor] cleaned up warnings in code and cleaned up pom.xml --- pom.xml | 315 ++++++++---------- .../java/ch/eitchnet/rmi/RmiFileDeletion.java | 4 +- .../java/ch/eitchnet/rmi/RmiFileHandler.java | 14 +- .../java/ch/eitchnet/rmi/RmiFilePart.java | 16 +- src/main/java/ch/eitchnet/rmi/RmiHelper.java | 51 +-- .../ch/eitchnet/utils/helper/FileHelper.java | 40 +-- .../utils/helper/Log4jConfigurator.java | 60 ++-- .../utils/helper/Log4jPropertyWatchDog.java | 32 +- .../eitchnet/utils/helper/ProcessHelper.java | 28 +- .../eitchnet/utils/helper/StringHelper.java | 61 ++-- .../eitchnet/utils/helper/SystemHelper.java | 32 +- .../utils/objectfilter/ObjectCache.java | 20 +- .../utils/objectfilter/ObjectFilter.java | 70 ++-- 13 files changed, 353 insertions(+), 390 deletions(-) diff --git a/pom.xml b/pom.xml index 20471eb4a..22a55833e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,192 +1,151 @@ - 4.0.0 - - ch.eitchnet - ch.eitchnet.utils - jar - 0.1.0-SNAPSHOT - ch.eitchnet.utils - https://github.com/eitch/ch.eitchnet.utils + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + 4.0.0 - - UTF-8 - + ch.eitchnet + ch.eitchnet.utils + jar + 0.1.0-SNAPSHOT + ch.eitchnet.utils + https://github.com/eitch/ch.eitchnet.utils - + + UTF-8 + - 2011 - - - GNU Lesser General Public License - http://www.gnu.org/licenses/lgpl.html - repo - - - - eitchnet.ch - http://blog.eitchnet.ch - - - - eitch - Robert von Vurg - eitch@eitchnet.ch - http://blog.eitchnet.ch - eitchnet.ch - http://blog.eitchnet.ch - - architect - developer - - +1 - - http://localhost - - - + - - Github Issues - https://github.com/eitch/ch.eitchnet.utils/issues - + 2011 + + + GNU Lesser General Public License + http://www.gnu.org/licenses/lgpl.html + repo + + + + eitchnet.ch + http://blog.eitchnet.ch + + + + eitch + Robert von Vurg + eitch@eitchnet.ch + http://blog.eitchnet.ch + eitchnet.ch + http://blog.eitchnet.ch + + architect + developer + + +1 + + http://localhost + + + - + - - scm:git:https://github.com/eitch/ch.eitchnet.utils.git - scm:git:git@github.com:eitch/ch.eitchnet.utils.git - https://github.com/eitch/ch.eitchnet.utils - + + scm:git:https://github.com/eitch/ch.eitchnet.utils.git + scm:git:git@github.com:eitch/ch.eitchnet.utils.git + https://github.com/eitch/ch.eitchnet.utils + - + - - - junit - junit - 4.10 - test - - - log4j - log4j - 1.2.17 - - + + + junit + junit + 4.10 + test + + + log4j + log4j + 1.2.17 + + - - - - - org.apache.maven.plugins - maven-eclipse-plugin - 2.9 - - true - true - - + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.0 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + attach-sources + verify + + jar-no-fork + + + + - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - - - - - org.apache.maven.plugins - maven-source-plugin - 2.1.2 - - - attach-sources - verify - - jar-no-fork - - - - + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + true + + + + + - - org.apache.maven.plugins - maven-jar-plugin - 2.4 - - - - true - true - - - - - - - - org.apache.maven.plugins - maven-site-plugin - 2.3 - - UTF-8 - - - - - + + org.apache.maven.plugins + maven-site-plugin + 2.3 + + UTF-8 + + + + diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java b/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java index a8fc6d385..3071092f0 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java @@ -47,13 +47,13 @@ public class RmiFileDeletion implements Serializable { * @return the fileType */ public String getFileType() { - return fileType; + return this.fileType; } /** * @return the fileName */ public String getFileName() { - return fileName; + return this.fileName; } } diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java index f21355aae..7d3ff04b4 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java @@ -80,7 +80,7 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File file = new File(basePath + "/" + fileType, filePart.getFileName()); + File file = new File(this.basePath + "/" + fileType, filePart.getFileName()); // now evaluate the file exists if (!file.canRead()) { @@ -102,9 +102,9 @@ public class RmiFileHandler { // variables defining the part of the file we're going to return long requestOffset = filePart.getPartOffset(); int requestSize = filePart.getPartLength(); - if (requestSize > MAX_PART_SIZE) { + if (requestSize > RmiFileHandler.MAX_PART_SIZE) { throw new RuntimeException("The requested part size " + requestSize + " is greater than the allowed " - + MAX_PART_SIZE); + + RmiFileHandler.MAX_PART_SIZE); } // validate lengths and offsets @@ -125,7 +125,7 @@ public class RmiFileHandler { long l = Math.min(requestSize, remaining); // this is a fail safe - if (l > MAX_PART_SIZE) + if (l > RmiFileHandler.MAX_PART_SIZE) throw new RuntimeException("Something went wrong. Min of requestSize and remaining is > MAX_PART_SIZE!"); // this is the size of the array we want to return @@ -163,7 +163,7 @@ public class RmiFileHandler { try { fin.close(); } catch (IOException e) { - logger.error("Error while closing FileInputStream: " + e.getLocalizedMessage()); + RmiFileHandler.logger.error("Error while closing FileInputStream: " + e.getLocalizedMessage()); } } } @@ -190,7 +190,7 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File dstFile = new File(basePath + "/" + fileType, filePart.getFileName()); + File dstFile = new File(this.basePath + "/" + fileType, filePart.getFileName()); // if the file already exists, then this may not be a start part if (filePart.getPartOffset() == 0 && dstFile.exists()) { @@ -231,7 +231,7 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File fileToDelete = new File(basePath + "/" + fileType, fileDeletion.getFileName()); + File fileToDelete = new File(this.basePath + "/" + fileType, fileDeletion.getFileName()); // delete the file return FileHelper.deleteFiles(new File[] { fileToDelete }, true); diff --git a/src/main/java/ch/eitchnet/rmi/RmiFilePart.java b/src/main/java/ch/eitchnet/rmi/RmiFilePart.java index 750b993a6..627fcc373 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFilePart.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFilePart.java @@ -64,7 +64,7 @@ public class RmiFilePart implements Serializable { * @return the fileLength */ public long getFileLength() { - return fileLength; + return this.fileLength; } /** @@ -79,7 +79,7 @@ public class RmiFilePart implements Serializable { * @return the fileHash */ public String getFileHash() { - return fileHash; + return this.fileHash; } /** @@ -94,14 +94,14 @@ public class RmiFilePart implements Serializable { * @return the fileType */ public String getFileType() { - return fileType; + return this.fileType; } /** * @return the partOffset */ public long getPartOffset() { - return partOffset; + return this.partOffset; } /** @@ -116,7 +116,7 @@ public class RmiFilePart implements Serializable { * @return the partLength */ public int getPartLength() { - return partLength; + return this.partLength; } /** @@ -131,7 +131,7 @@ public class RmiFilePart implements Serializable { * @return the partBytes */ public byte[] getPartBytes() { - return partBytes; + return this.partBytes; } /** @@ -146,7 +146,7 @@ public class RmiFilePart implements Serializable { * @return the lastPart */ public boolean isLastPart() { - return lastPart; + return this.lastPart; } /** @@ -161,6 +161,6 @@ public class RmiFilePart implements Serializable { * @return the fileName */ public String getFileName() { - return fileName; + return this.fileName; } } diff --git a/src/main/java/ch/eitchnet/rmi/RmiHelper.java b/src/main/java/ch/eitchnet/rmi/RmiHelper.java index a8530ad0c..6cf6dae2a 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiHelper.java +++ b/src/main/java/ch/eitchnet/rmi/RmiHelper.java @@ -40,10 +40,10 @@ public class RmiHelper { /** * @param rmiFileClient - * @param filePart + * @param origFilePart * @param dstFile */ - public static void downloadFile(RMIFileClient rmiFileClient, RmiFilePart filePart, File dstFile) { + public static void downloadFile(RMIFileClient rmiFileClient, RmiFilePart origFilePart, File dstFile) { // here we don't overwrite, the caller must make sure the destination file does not exist if (dstFile.exists()) @@ -51,56 +51,59 @@ public class RmiHelper { + " already exists. Delete it first, if you want to overwrite it!"); try { + + RmiFilePart tmpPart = origFilePart; + int loops = 0; - int startLength = filePart.getPartLength(); + int startLength = tmpPart.getPartLength(); while (true) { loops += 1; // get the next part - filePart = rmiFileClient.requestFile(filePart); + tmpPart = rmiFileClient.requestFile(tmpPart); // validate length of data - if (filePart.getPartLength() != filePart.getPartBytes().length) - throw new RuntimeException("Invalid FilePart. Part length is not as long as the bytes passed " - + filePart.getPartLength() + " / " + filePart.getPartBytes().length); + if (tmpPart.getPartLength() != tmpPart.getPartBytes().length) + throw new RuntimeException("Invalid tmpPart. Part length is not as long as the bytes passed " + + tmpPart.getPartLength() + " / " + tmpPart.getPartBytes().length); // validate offset is size of file - if (filePart.getPartOffset() != dstFile.length()) { + if (tmpPart.getPartOffset() != dstFile.length()) { throw new RuntimeException("The part offset $offset is not at the end of the file " - + filePart.getPartOffset() + " / " + dstFile.length()); + + tmpPart.getPartOffset() + " / " + dstFile.length()); } // append the part - FileHelper.appendFilePart(dstFile, filePart.getPartBytes()); + FileHelper.appendFilePart(dstFile, tmpPart.getPartBytes()); // update the offset - filePart.setPartOffset(filePart.getPartOffset() + filePart.getPartBytes().length); + tmpPart.setPartOffset(tmpPart.getPartOffset() + tmpPart.getPartBytes().length); // break if the offset is past the length of the file - if (filePart.getPartOffset() >= filePart.getFileLength()) + if (tmpPart.getPartOffset() >= tmpPart.getFileLength()) break; } - logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Requested " + loops - + " parts. StartSize: " + startLength + " EndSize: " + filePart.getPartLength()); + RmiHelper.logger.info(tmpPart.getFileType() + ": " + tmpPart.getFileName() + ": Requested " + loops + + " parts. StartSize: " + startLength + " EndSize: " + tmpPart.getPartLength()); // validate that the offset is at the end of the file - if (filePart.getPartOffset() != filePart.getFileLength()) { - throw new RuntimeException("Offset " + filePart.getPartOffset() + " is not at file length " - + filePart.getFileLength() + " after reading all the file parts!"); + if (tmpPart.getPartOffset() != origFilePart.getFileLength()) { + throw new RuntimeException("Offset " + tmpPart.getPartOffset() + " is not at file length " + + origFilePart.getFileLength() + " after reading all the file parts!"); } // now validate hashes String dstFileHash = StringHelper.getHexString(FileHelper.hashFileSha256(dstFile)); - if (!dstFileHash.equals(filePart.getFileHash())) { - throw new RuntimeException("Downloading the file " + filePart.getFileName() - + " failed because the hashes don't match. Expected: " + filePart.getFileHash() + " / Actual: " - + dstFileHash); + if (!dstFileHash.equals(origFilePart.getFileHash())) { + throw new RuntimeException("Downloading the file " + origFilePart.getFileName() + + " failed because the hashes don't match. Expected: " + origFilePart.getFileHash() + + " / Actual: " + dstFileHash); } } catch (Exception e) { if (e instanceof RuntimeException) throw (RuntimeException) e; - throw new RuntimeException("Downloading the file " + filePart.getFileName() + throw new RuntimeException("Downloading the file " + origFilePart.getFileName() + " failed because of an underlying exception " + e.getLocalizedMessage()); } } @@ -194,7 +197,7 @@ public class RmiHelper { offset = nextOffset; } - logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Sent " + loops + RmiHelper.logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Sent " + loops + " parts. StartSize: " + startLength + " EndSize: " + filePart.getPartLength()); } catch (Exception e) { @@ -207,7 +210,7 @@ public class RmiHelper { try { inputStream.close(); } catch (IOException e) { - logger.error("Exception while closing FileInputStream " + e.getLocalizedMessage()); + RmiHelper.logger.error("Exception while closing FileInputStream " + e.getLocalizedMessage()); } } } diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 9cba18dc3..aac499b67 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -79,7 +79,7 @@ public class FileHelper { try { bufferedReader.close(); } catch (IOException e) { - logger.error("Failed to close BufferedReader: " + e.getLocalizedMessage()); + FileHelper.logger.error("Failed to close BufferedReader: " + e.getLocalizedMessage()); } } } @@ -115,7 +115,7 @@ public class FileHelper { * @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 deleteFiles(new File[] { file }, log); + return FileHelper.deleteFiles(new File[] { file }, log); } /** @@ -132,28 +132,28 @@ public class FileHelper { for (int i = 0; i < files.length; i++) { File file = files[i]; if (file.isDirectory()) { - boolean done = deleteFiles(file.listFiles(), log); + boolean done = FileHelper.deleteFiles(file.listFiles(), log); if (!done) { worked = false; - logger.warn("Could not empty the directory: " + file.getAbsolutePath()); + FileHelper.logger.warn("Could not empty the directory: " + file.getAbsolutePath()); } else { done = file.delete(); if (done) { if (log) - logger.info("Deleted DIR " + file.getAbsolutePath()); + FileHelper.logger.info("Deleted DIR " + file.getAbsolutePath()); } else { worked = false; - logger.warn("Could not delete the directory: " + file.getAbsolutePath()); + FileHelper.logger.warn("Could not delete the directory: " + file.getAbsolutePath()); } } } else { boolean done = file.delete(); if (done) { if (log) - logger.info("Deleted FILE " + file.getAbsolutePath()); + FileHelper.logger.info("Deleted FILE " + file.getAbsolutePath()); } else { worked = false; - logger.warn(("Could not delete the file: " + file.getAbsolutePath())); + FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath())); } } } @@ -195,7 +195,7 @@ public class FileHelper { String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile)); String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile)); if (!fromFileMD5.equals(toFileMD5)) { - logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " + toFileMD5); + FileHelper.logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " + toFileMD5); toFile.delete(); return false; @@ -204,7 +204,7 @@ public class FileHelper { // cleanup if files are not the same length if (fromFile.length() != toFile.length()) { - logger.error("Copying failed, as new files are not the same length: " + fromFile.length() + " / " + FileHelper.logger.error("Copying failed, as new files are not the same length: " + fromFile.length() + " / " + toFile.length()); toFile.delete(); @@ -213,7 +213,7 @@ public class FileHelper { } catch (Exception e) { - logger.error(e, e); + FileHelper.logger.error(e, e); return false; } finally { @@ -222,7 +222,7 @@ public class FileHelper { try { inBuffer.close(); } catch (IOException e) { - logger.error("Error closing BufferedInputStream" + e); + FileHelper.logger.error("Error closing BufferedInputStream" + e); } } @@ -230,7 +230,7 @@ public class FileHelper { try { outBuffer.close(); } catch (IOException e) { - logger.error("Error closing BufferedOutputStream" + e); + FileHelper.logger.error("Error closing BufferedOutputStream" + e); } } } @@ -254,11 +254,11 @@ public class FileHelper { return true; } - logger.warn("Simple File.renameTo failed, trying copy/delete..."); + FileHelper.logger.warn("Simple File.renameTo failed, trying copy/delete..."); // delete if copy was successful, otherwise move will fail if (FileHelper.copy(fromFile, toFile, true)) { - logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); + FileHelper.logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); return fromFile.delete(); } @@ -372,7 +372,7 @@ public class FileHelper { * @return the humanized form of the files size */ public final static String humanizeFileSize(File file) { - return humanizeFileSize(file.length()); + return FileHelper.humanizeFileSize(file.length()); } /** @@ -407,7 +407,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileMd5(File file) { - return hashFile(file, "MD5"); + return FileHelper.hashFile(file, "MD5"); } /** @@ -420,7 +420,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileSha1(File file) { - return hashFile(file, "SHA-1"); + return FileHelper.hashFile(file, "SHA-1"); } /** @@ -433,7 +433,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileSha256(File file) { - return hashFile(file, "SHA-256"); + return FileHelper.hashFile(file, "SHA-256"); } /** @@ -492,7 +492,7 @@ public class FileHelper { try { outputStream.close(); } catch (IOException e) { - logger.error("Exception while closing FileOutputStream " + e.getLocalizedMessage()); + FileHelper.logger.error("Exception while closing FileOutputStream " + e.getLocalizedMessage()); } } } diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java index 327ab5446..678ff78f3 100644 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java +++ b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java @@ -63,9 +63,9 @@ public class Log4jConfigurator { * Configures log4j with the default {@link ConsoleAppender} */ public static synchronized void configure() { - cleanupOldWatchdog(); + Log4jConfigurator.cleanupOldWatchdog(); BasicConfigurator.resetConfiguration(); - BasicConfigurator.configure(new ConsoleAppender(getDefaulLayout())); + BasicConfigurator.configure(new ConsoleAppender(Log4jConfigurator.getDefaulLayout())); Logger.getRootLogger().setLevel(Level.INFO); } @@ -119,13 +119,13 @@ public class Log4jConfigurator { + log4JPath.getAbsolutePath()); // now perform the loading - loadLog4jConfiguration(log4JPath); + Log4jConfigurator.loadLog4jConfiguration(log4JPath); } catch (Exception e) { Log4jConfigurator.configure(); - logger.error(e, e); - logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file exists at " + Log4jConfigurator.logger.error(e, e); + Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file exists at " + log4JPath.getAbsolutePath()); } @@ -152,7 +152,7 @@ public class Log4jConfigurator { throw new RuntimeException("log4jConfigPath may not be null!"); // first clean up any old watch dog in case of a programmatic re-load of the configuration - cleanupOldWatchdog(); + Log4jConfigurator.cleanupOldWatchdog(); // get the root directory String userDir = System.getProperty("user.dir"); @@ -184,16 +184,16 @@ public class Log4jConfigurator { // XXX if the server is in a web context, then we may not use the // FileWatchDog BasicConfigurator.resetConfiguration(); - watchDog = new Log4jPropertyWatchDog(pathNameToLog4jTemp); - watchDog.start(); + Log4jConfigurator.watchDog = new Log4jPropertyWatchDog(pathNameToLog4jTemp); + Log4jConfigurator.watchDog.start(); - logger.info("Log4j is configured to use and watch file " + pathNameToLog4jTemp); + Log4jConfigurator.logger.info("Log4j is configured to use and watch file " + pathNameToLog4jTemp); } catch (Exception e) { Log4jConfigurator.configure(); - logger.error(e, e); - logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file at " + Log4jConfigurator.logger.error(e, e); + Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file at " + log4jConfigPath); } finally { @@ -201,14 +201,14 @@ public class Log4jConfigurator { try { fin.close(); } catch (IOException e) { - logger.error("Exception closing input file: " + e, e); + Log4jConfigurator.logger.error("Exception closing input file: " + e, e); } } if (fout != null) { try { fout.close(); } catch (IOException e) { - logger.error("Exception closing output file: " + e, e); + Log4jConfigurator.logger.error("Exception closing output file: " + e, e); } } } @@ -229,9 +229,9 @@ public class Log4jConfigurator { if (clazz == null) throw new RuntimeException("clazz may not be null!"); - InputStream resourceAsStream = clazz.getResourceAsStream("/" + FILE_LOG4J); + InputStream resourceAsStream = clazz.getResourceAsStream("/" + Log4jConfigurator.FILE_LOG4J); if (resourceAsStream == null) { - throw new RuntimeException("The resource '" + FILE_LOG4J + "' could not be found for class " + throw new RuntimeException("The resource '" + Log4jConfigurator.FILE_LOG4J + "' could not be found for class " + clazz.getName()); } @@ -240,13 +240,13 @@ public class Log4jConfigurator { log4jProperties.load(resourceAsStream); // and then - loadLog4jConfiguration(log4jProperties); + Log4jConfigurator.loadLog4jConfiguration(log4jProperties); } catch (Exception e) { Log4jConfigurator.configure(); - logger.error(e, e); - logger.error("Log4j COULD NOT BE INITIALIZED. Please check that the log4j configuration file '" - + FILE_LOG4J + "' exists as a resource for class " + clazz.getName() + Log4jConfigurator.logger.error(e, e); + Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check that the log4j configuration file '" + + Log4jConfigurator.FILE_LOG4J + "' exists as a resource for class " + clazz.getName() + " and is a valid properties configuration"); } } @@ -272,19 +272,19 @@ public class Log4jConfigurator { throw new RuntimeException("log4jProperties may not be null!"); // first clean up any old watch dog in case of a programmatic re-load of the configuration - cleanupOldWatchdog(); + Log4jConfigurator.cleanupOldWatchdog(); // replace any variables StringHelper.replaceProperties(log4jProperties, System.getProperties()); // now configure log4j PropertyConfigurator.configure(log4jProperties); - logger.info("Log4j is configured using the given properties."); + Log4jConfigurator.logger.info("Log4j is configured using the given properties."); } catch (Exception e) { Log4jConfigurator.configure(); - logger.error(e, e); - logger.error("Log4j COULD NOT BE INITIALIZED. The given log4jProperties seem not to be valid!"); + Log4jConfigurator.logger.error(e, e); + Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. The given log4jProperties seem not to be valid!"); } } @@ -293,17 +293,17 @@ public class Log4jConfigurator { */ public static synchronized void cleanupOldWatchdog() { // clean up an old watch dog - if (watchDog != null) { - logger.info("Stopping old Log4j watchdog."); - watchDog.interrupt(); + if (Log4jConfigurator.watchDog != null) { + Log4jConfigurator.logger.info("Stopping old Log4j watchdog."); + Log4jConfigurator.watchDog.interrupt(); try { - watchDog.join(1000l); + Log4jConfigurator.watchDog.join(1000l); } catch (InterruptedException e) { - logger.error("Oops. Could not terminate an old WatchDog."); + Log4jConfigurator.logger.error("Oops. Could not terminate an old WatchDog."); } finally { - watchDog = null; + Log4jConfigurator.watchDog = null; } - logger.info("Done."); + Log4jConfigurator.logger.info("Done."); } } } diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java index a4f2aecda..e13794550 100644 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java +++ b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java @@ -44,7 +44,7 @@ public class Log4jPropertyWatchDog extends Thread { /** * The delay to observe between every check. By default set {@link #DEFAULT_DELAY}. */ - protected long delay = DEFAULT_DELAY; + protected long delay = Log4jPropertyWatchDog.DEFAULT_DELAY; protected File file; protected long lastModif = 0; @@ -57,7 +57,7 @@ public class Log4jPropertyWatchDog extends Thread { protected Log4jPropertyWatchDog(String filename) { super("FileWatchdog"); this.filename = filename; - file = new File(filename); + this.file = new File(filename); setDaemon(true); checkAndConfigure(); } @@ -75,24 +75,24 @@ public class Log4jPropertyWatchDog extends Thread { protected void checkAndConfigure() { boolean fileExists; try { - fileExists = file.exists(); + fileExists = this.file.exists(); } catch (SecurityException e) { - LogLog.warn("Was not allowed to read check file existance, file:[" + filename + "]."); - interrupted = true; // there is no point in continuing + LogLog.warn("Was not allowed to read check file existance, file:[" + this.filename + "]."); + this.interrupted = true; // there is no point in continuing return; } if (fileExists) { - long l = file.lastModified(); // this can also throw a SecurityException - if (l > lastModif) { // however, if we reached this point this - lastModif = l; // is very unlikely. + long l = this.file.lastModified(); // this can also throw a SecurityException + if (l > this.lastModif) { // however, if we reached this point this + this.lastModif = l; // is very unlikely. doOnChange(); - warnedAlready = false; + this.warnedAlready = false; } } else { - if (!warnedAlready) { - LogLog.debug("[" + filename + "] does not exist."); - warnedAlready = true; + if (!this.warnedAlready) { + LogLog.debug("[" + this.filename + "] does not exist."); + this.warnedAlready = true; } } } @@ -102,7 +102,7 @@ public class Log4jPropertyWatchDog extends Thread { */ public void doOnChange() { PropertyConfigurator propertyConfigurator = new PropertyConfigurator(); - propertyConfigurator.doConfigure(filename, LogManager.getLoggerRepository()); + propertyConfigurator.doConfigure(this.filename, LogManager.getLoggerRepository()); } /** @@ -110,12 +110,12 @@ public class Log4jPropertyWatchDog extends Thread { */ @Override public void run() { - while (!interrupted) { + while (!this.interrupted) { try { - Thread.sleep(delay); + Thread.sleep(this.delay); } catch (InterruptedException e) { // no interruption expected - interrupted = true; + this.interrupted = true; } checkAndConfigure(); } diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 30a7a17bd..c30366426 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -45,7 +45,7 @@ public class ProcessHelper { Thread errorIn = new Thread("errorIn") { @Override public void run() { - readStream(sb, "[ERROR] ", errorStream); + ProcessHelper.readStream(sb, "[ERROR] ", errorStream); } }; errorIn.start(); @@ -55,7 +55,7 @@ public class ProcessHelper { Thread infoIn = new Thread("infoIn") { @Override public void run() { - readStream(sb, "[INFO] ", inputStream); + ProcessHelper.readStream(sb, "[INFO] ", inputStream); } }; infoIn.start(); @@ -72,7 +72,7 @@ public class ProcessHelper { throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); } catch (InterruptedException e) { - logger.error("Interrupted!"); + ProcessHelper.logger.error("Interrupted!"); sb.append("[FATAL] Interrupted"); return new ProcessResult(-1, sb.toString(), e); } @@ -102,7 +102,7 @@ public class ProcessHelper { Thread errorIn = new Thread("errorIn") { @Override public void run() { - readStream(sb, "[ERROR] ", errorStream); + ProcessHelper.readStream(sb, "[ERROR] ", errorStream); } }; errorIn.start(); @@ -112,7 +112,7 @@ public class ProcessHelper { Thread infoIn = new Thread("infoIn") { @Override public void run() { - readStream(sb, "[INFO] ", inputStream); + ProcessHelper.readStream(sb, "[INFO] ", inputStream); } }; infoIn.start(); @@ -129,7 +129,7 @@ public class ProcessHelper { throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); } catch (InterruptedException e) { - logger.error("Interrupted!"); + ProcessHelper.logger.error("Interrupted!"); sb.append("[FATAL] Interrupted"); return new ProcessResult(-1, sb.toString(), e); } @@ -165,33 +165,33 @@ public class ProcessHelper { ProcessResult processResult; if (SystemHelper.isLinux()) { - processResult = runCommand("xdg-open " + pdfPath.getAbsolutePath()); + processResult = ProcessHelper.runCommand("xdg-open " + pdfPath.getAbsolutePath()); } else if (SystemHelper.isMacOS()) { - processResult = runCommand("open " + pdfPath.getAbsolutePath()); + processResult = ProcessHelper.runCommand("open " + pdfPath.getAbsolutePath()); } 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 " + processResult = ProcessHelper.runCommand("rundll32 url.dll,FileProtocolHandler " + pdfFile); } else { throw new UnsupportedOperationException("Unexpected OS: " + SystemHelper.osName); } - logProcessResult(processResult); + ProcessHelper.logProcessResult(processResult); } public static void logProcessResult(ProcessResult processResult) { if (processResult.returnValue == 0) { - logger.info("Process executed successfully"); + ProcessHelper.logger.info("Process executed successfully"); } else if (processResult.returnValue == -1) { - logger.error("Process execution failed:\n" + ProcessHelper.logger.error("Process execution failed:\n" + processResult.processOutput); - logger.error(processResult.t, processResult.t); + ProcessHelper.logger.error(processResult.t, processResult.t); } else { - logger.info("Process execution was not successful with return value:" + ProcessHelper.logger.info("Process execution was not successful with return value:" + processResult.returnValue + "\n" + processResult.processOutput); diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index c44a2ac5e..9ba4b56fb 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -59,8 +59,8 @@ public class StringHelper { for (byte b : raw) { int v = b & 0xFF; - hex[index++] = HEX_CHAR_TABLE[v >>> 4]; - hex[index++] = HEX_CHAR_TABLE[v & 0xF]; + hex[index++] = StringHelper.HEX_CHAR_TABLE[v >>> 4]; + hex[index++] = StringHelper.HEX_CHAR_TABLE[v & 0xF]; } return new String(hex, "ASCII"); @@ -103,7 +103,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(String string) { - return hashMd5(string.getBytes()); + return StringHelper.hashMd5(string.getBytes()); } /** @@ -116,7 +116,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(byte[] bytes) { - return hash("MD5", bytes); + return StringHelper.hash("MD5", bytes); } /** @@ -129,7 +129,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(String string) { - return hashSha1(string.getBytes()); + return StringHelper.hashSha1(string.getBytes()); } /** @@ -142,7 +142,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(byte[] bytes) { - return hash("SHA-1", bytes); + return StringHelper.hash("SHA-1", bytes); } /** @@ -155,7 +155,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(String string) { - return hashSha256(string.getBytes()); + return StringHelper.hashSha256(string.getBytes()); } /** @@ -168,7 +168,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(byte[] bytes) { - return hash("SHA-256", bytes); + return StringHelper.hash("SHA-256", bytes); } /** @@ -209,7 +209,7 @@ public class StringHelper { * @return the new string */ public static String normalizeLength(String value, int length, boolean beginning, char c) { - return normalizeLength(value, length, beginning, false, c); + return StringHelper.normalizeLength(value, length, beginning, false, c); } /** @@ -235,20 +235,21 @@ public class StringHelper { if (value.length() < length) { - while (value.length() != length) { + String tmp = value; + while (tmp.length() != length) { if (beginning) { - value = c + value; + tmp = c + tmp; } else { - value = value + c; + tmp = tmp + c; } } - return value; + return tmp; } else if (shorten) { - logger.warn("Shortening length of value: " + value); - logger.warn("Length is: " + value.length() + " max: " + length); + StringHelper.logger.warn("Shortening length of value: " + value); + StringHelper.logger.warn("Length is: " + value.length() + " max: " + length); return value.substring(0, length); } @@ -263,7 +264,7 @@ public class StringHelper { * returned */ public static String replaceSystemPropertiesIn(String value) { - return replacePropertiesIn(System.getProperties(), value); + return StringHelper.replacePropertiesIn(System.getProperties(), value); } /** @@ -278,41 +279,41 @@ public class StringHelper { * * @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) { + public static String replacePropertiesIn(Properties properties, String alue) { - // keep copy of original value - String origValue = value; + // get a copy of the value + String tmpValue = alue; // get first occurrence of $ character int pos = -1; int stop = 0; // loop on $ character positions - while ((pos = value.indexOf('$', pos + 1)) != -1) { + while ((pos = tmpValue.indexOf('$', pos + 1)) != -1) { // if pos+1 is not { character then continue - if (value.charAt(pos + 1) != '{') { + if (tmpValue.charAt(pos + 1) != '{') { continue; } // find end of sequence with } character - stop = value.indexOf('}', pos + 1); + stop = tmpValue.indexOf('}', pos + 1); // if no stop found, then break as another sequence should be able to start if (stop == -1) { - logger.error("Sequence starts at offset " + pos + " but does not end!"); - value = origValue; + StringHelper.logger.error("Sequence starts at offset " + pos + " but does not end!"); + tmpValue = alue; break; } // get sequence enclosed by pos and stop - String sequence = value.substring(pos + 2, stop); + String sequence = tmpValue.substring(pos + 2, stop); // make sure sequence doesn't contain $ { } characters if (sequence.contains("$") || sequence.contains("{") || sequence.contains("}")) { - logger.error("Enclosed sequence in offsets " + pos + " - " + stop + StringHelper.logger.error("Enclosed sequence in offsets " + pos + " - " + stop + " contains one of the illegal chars: $ { }: " + sequence); - value = origValue; + tmpValue = alue; break; } @@ -326,10 +327,10 @@ public class StringHelper { } // property exists, so replace in value - value = value.replace("${" + sequence + "}", property); + tmpValue = tmpValue.replace("${" + sequence + "}", property); } - return value; + return tmpValue; } /** @@ -340,7 +341,7 @@ public class StringHelper { * 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); + StringHelper.replaceProperties(properties, null); } /** diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index eeb3aa284..50fb91628 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -34,7 +34,7 @@ public class SystemHelper { } public static SystemHelper getInstance() { - return instance; + return SystemHelper.instance; } public static final String osName = System.getProperty("os.name"); @@ -55,7 +55,7 @@ public class SystemHelper { */ @Override public String toString() { - return asString(); + return SystemHelper.asString(); } /** @@ -63,15 +63,15 @@ public class SystemHelper { */ public static String asString() { StringBuilder sb = new StringBuilder(); - sb.append(osName); + sb.append(SystemHelper.osName); sb.append(" "); - sb.append(osArch); + sb.append(SystemHelper.osArch); sb.append(" "); - sb.append(osVersion); + sb.append(SystemHelper.osVersion); sb.append(" "); - sb.append("on Java " + javaVendor); + sb.append("on Java " + SystemHelper.javaVendor); sb.append(" version "); - sb.append(javaVersion); + sb.append(SystemHelper.javaVersion); return sb.toString(); } @@ -80,23 +80,23 @@ public class SystemHelper { } public static boolean isMacOS() { - return osName.startsWith("Mac"); + return SystemHelper.osName.startsWith("Mac"); } public static boolean isWindows() { - return osName.startsWith("Win"); + return SystemHelper.osName.startsWith("Win"); } public static boolean isLinux() { - return osName.startsWith("Lin"); + return SystemHelper.osName.startsWith("Lin"); } public static boolean is32bit() { - return osArch.equals("x86") || osArch.equals("i386") || osArch.equals("i686"); + return SystemHelper.osArch.equals("x86") || SystemHelper.osArch.equals("i386") || SystemHelper.osArch.equals("i686"); } public static boolean is64bit() { - return osArch.equals("x86_64") || osArch.equals("amd64"); + return SystemHelper.osArch.equals("x86_64") || SystemHelper.osArch.equals("amd64"); } public static String getMaxMemory() { @@ -112,7 +112,7 @@ public class SystemHelper { } public static String getMemorySummary() { - return "Memory available " + getMaxMemory() + " / Used: " + getUsedMemory() + " / Free:" + getFreeMemory(); + return "Memory available " + SystemHelper.getMaxMemory() + " / Used: " + SystemHelper.getUsedMemory() + " / Free:" + SystemHelper.getFreeMemory(); } /** @@ -150,7 +150,7 @@ public class SystemHelper { * 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())); + return Boolean.valueOf(SystemHelper.getProperty(context, key, def == null ? null : def.toString())); } /** @@ -163,7 +163,7 @@ public class SystemHelper { * 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())); + return Integer.valueOf(SystemHelper.getProperty(context, key, def == null ? null : def.toString())); } /** @@ -176,6 +176,6 @@ public class SystemHelper { * 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())); + return Double.valueOf(SystemHelper.getProperty(context, key, def == null ? null : def.toString())); } } diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index e2d0b5e09..2c55d2705 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -71,8 +71,8 @@ public class ObjectCache { this.object = object; this.operation = operation; - if (logger.isDebugEnabled()) { - logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + " / " + if (ObjectCache.logger.isDebugEnabled()) { + ObjectCache.logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + " / " + object.toString()); } } @@ -83,8 +83,8 @@ public class ObjectCache { * @param object */ public void setObject(T object) { - if (logger.isDebugEnabled()) { - logger.debug("Updating ID " + this.id + " to value " + object.toString()); + if (ObjectCache.logger.isDebugEnabled()) { + ObjectCache.logger.debug("Updating ID " + this.id + " to value " + object.toString()); } this.object = object; } @@ -95,8 +95,8 @@ public class ObjectCache { * @param newOperation */ public void setOperation(Operation newOperation) { - if (logger.isDebugEnabled()) { - logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " + newOperation); + if (ObjectCache.logger.isDebugEnabled()) { + ObjectCache.logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " + newOperation); } this.operation = newOperation; } @@ -105,27 +105,27 @@ public class ObjectCache { * @return the id */ public long getId() { - return id; + return this.id; } /** * @return the key */ public String getKey() { - return key; + return this.key; } /** * @return the object */ public T getObject() { - return object; + return this.object; } /** * @return the operation */ public Operation getOperation() { - return operation; + return this.operation; } } diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index a79970c11..51733db77 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -117,11 +117,11 @@ public class ObjectFilter { */ public void add(String key, T objectToAdd) { - if (logger.isDebugEnabled()) - logger.debug("add object " + objectToAdd + " with key " + key); + if (ObjectFilter.logger.isDebugEnabled()) + ObjectFilter.logger.debug("add object " + objectToAdd + " with key " + key); // add the key to the set - keySet.add(key); + this.keySet.add(key); // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. long id = objectToAdd.getTransactionID(); @@ -131,14 +131,14 @@ public class ObjectFilter { id = dispenseID(); objectToAdd.setTransactionID(id); ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { - ObjectCache cached = cache.get(Long.valueOf(objectToAdd.getTransactionID())); + ObjectCache cached = this.cache.get(Long.valueOf(objectToAdd.getTransactionID())); if (cached == null) { // The object got an ID during this run, but was not added to the cache. // Hence, we add it now, with the current operation. ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { @@ -197,11 +197,11 @@ public class ObjectFilter { */ public void update(String key, T objectToUpdate) { - if (logger.isDebugEnabled()) - logger.debug("update object " + objectToUpdate + " with key " + key); + if (ObjectFilter.logger.isDebugEnabled()) + ObjectFilter.logger.debug("update object " + objectToUpdate + " with key " + key); // add the key to the keyset - keySet.add(key); + this.keySet.add(key); // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. long id = objectToUpdate.getTransactionID(); @@ -209,14 +209,14 @@ public class ObjectFilter { id = dispenseID(); objectToUpdate.setTransactionID(id); ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { - ObjectCache cached = cache.get(Long.valueOf(objectToUpdate.getTransactionID())); + ObjectCache cached = this.cache.get(Long.valueOf(objectToUpdate.getTransactionID())); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { @@ -275,25 +275,25 @@ public class ObjectFilter { */ public void remove(String key, T objectToRemove) { - if (logger.isDebugEnabled()) - logger.debug("remove object " + objectToRemove + " with key " + key); + if (ObjectFilter.logger.isDebugEnabled()) + ObjectFilter.logger.debug("remove object " + objectToRemove + " with key " + key); // add the key to the keyset - keySet.add(key); + this.keySet.add(key); // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. long id = objectToRemove.getTransactionID(); if (id == ITransactionObject.UNSET) { id = dispenseID(); objectToRemove.setTransactionID(id); ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { - ObjectCache cached = cache.get(Long.valueOf(id)); + ObjectCache cached = this.cache.get(Long.valueOf(id)); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); - cache.put(id, cacheObj); + this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { @@ -315,7 +315,7 @@ public class ObjectFilter { case ADD: // this is a case where we're removing the object from the cache, since we are // removing it now and it was added previously. - cache.remove(Long.valueOf(id)); + this.cache.remove(Long.valueOf(id)); break; case MODIFY: cached.setObject(objectToRemove); @@ -448,7 +448,7 @@ public class ObjectFilter { */ public List getAdded(String key) { List addedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { addedObjects.add(objectCache.getObject()); @@ -468,7 +468,7 @@ public class ObjectFilter { */ public List getAdded(Class clazz, String key) { List addedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @@ -490,7 +490,7 @@ public class ObjectFilter { */ public List getUpdated(String key) { List updatedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { updatedObjects.add(objectCache.getObject()); @@ -508,7 +508,7 @@ public class ObjectFilter { */ public List getUpdated(Class clazz, String key) { List updatedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @@ -530,7 +530,7 @@ public class ObjectFilter { */ public List getRemoved(String key) { List removedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { removedObjects.add(objectCache.getObject()); @@ -548,7 +548,7 @@ public class ObjectFilter { */ public List getRemoved(Class clazz, String key) { List removedObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @@ -571,7 +571,7 @@ public class ObjectFilter { */ public List getAll(String key) { List allObjects = new LinkedList(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allObjects.add(objectCache.getObject()); @@ -590,7 +590,7 @@ public class ObjectFilter { */ public List> getCache(String key) { List> allCache = new LinkedList>(); - Collection> allObjs = cache.values(); + Collection> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allCache.add(objectCache); @@ -605,26 +605,26 @@ public class ObjectFilter { * @return The set containing the keys of that have been added to the filter. */ public Set keySet() { - return keySet; + return this.keySet; } /** * Clear the cache. */ public void clearCache() { - cache.clear(); - keySet.clear(); + this.cache.clear(); + this.keySet.clear(); } /** * @return get a unique transaction ID */ public synchronized long dispenseID() { - id++; - if (id == Long.MAX_VALUE) { - logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); - id = 1; + ObjectFilter.id++; + if (ObjectFilter.id == Long.MAX_VALUE) { + ObjectFilter.logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); + ObjectFilter.id = 1; } - return id; + return ObjectFilter.id; } } From 3f5bf2d33434df7b1218773a4a7b3ef87416c1d2 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 24 Nov 2012 13:22:40 +0100 Subject: [PATCH 012/180] [Major] refactored use of log4j to slf4j --- pom.xml | 14 +- .../java/ch/eitchnet/rmi/RmiFileHandler.java | 5 +- src/main/java/ch/eitchnet/rmi/RmiHelper.java | 5 +- .../ch/eitchnet/utils/helper/FileHelper.java | 16 +- .../utils/helper/Log4jConfigurator.java | 309 ------ .../utils/helper/Log4jPropertyWatchDog.java | 123 --- .../eitchnet/utils/helper/ProcessHelper.java | 50 +- .../eitchnet/utils/helper/StringHelper.java | 931 +++++++++--------- .../eitchnet/utils/helper/SystemHelper.java | 9 +- .../utils/objectfilter/ObjectCache.java | 12 +- .../utils/objectfilter/ObjectFilter.java | 7 +- 11 files changed, 525 insertions(+), 956 deletions(-) delete mode 100644 src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java delete mode 100644 src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java diff --git a/pom.xml b/pom.xml index 22a55833e..722434b6e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,7 @@ jar 0.1.0-SNAPSHOT ch.eitchnet.utils + These utils contain project independent helper classes and utilities for reuse https://github.com/eitch/ch.eitchnet.utils @@ -82,9 +83,15 @@ test - log4j - log4j - 1.2.17 + org.slf4j + slf4j-api + 1.7.2 + + + org.slf4j + slf4j-log4j12 + 1.7.2 + test @@ -132,7 +139,6 @@ true true - diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java index 7d3ff04b4..6dcc256b4 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java +++ b/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java @@ -24,7 +24,8 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ch.eitchnet.utils.helper.FileHelper; import ch.eitchnet.utils.helper.StringHelper; @@ -37,7 +38,7 @@ import ch.eitchnet.utils.helper.StringHelper; */ public class RmiFileHandler { - private static final Logger logger = Logger.getLogger(RmiFileHandler.class); + private static final Logger logger = LoggerFactory.getLogger(RmiFileHandler.class); /** * DEF_PART_SIZE = default part size which is set to 1048576 bytes (1 MiB) diff --git a/src/main/java/ch/eitchnet/rmi/RmiHelper.java b/src/main/java/ch/eitchnet/rmi/RmiHelper.java index 6cf6dae2a..049e4dbd7 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiHelper.java +++ b/src/main/java/ch/eitchnet/rmi/RmiHelper.java @@ -25,7 +25,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.rmi.RemoteException; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ch.eitchnet.utils.helper.FileHelper; import ch.eitchnet.utils.helper.StringHelper; @@ -36,7 +37,7 @@ import ch.eitchnet.utils.helper.StringHelper; */ public class RmiHelper { - private static final Logger logger = Logger.getLogger(RmiHelper.class); + private static final Logger logger = LoggerFactory.getLogger(RmiHelper.class); /** * @param rmiFileClient diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index aac499b67..0bcbbf1f6 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -36,7 +36,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Helper class for dealing with files @@ -45,7 +46,7 @@ import org.apache.log4j.Logger; */ public class FileHelper { - private static final Logger logger = Logger.getLogger(FileHelper.class); + private static final Logger logger = LoggerFactory.getLogger(FileHelper.class); /** * Reads the contents of a file into a string. Note, no encoding is checked. It is expected to be UTF-8 @@ -55,7 +56,7 @@ public class FileHelper { * @return the contents of a file as a string */ public static final String readFileToString(File file) { - + BufferedReader bufferedReader = null; try { @@ -195,7 +196,8 @@ public class FileHelper { String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile)); String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile)); if (!fromFileMD5.equals(toFileMD5)) { - FileHelper.logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " + toFileMD5); + FileHelper.logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " + + toFileMD5); toFile.delete(); return false; @@ -204,8 +206,8 @@ public class FileHelper { // cleanup if files are not the same length if (fromFile.length() != toFile.length()) { - FileHelper.logger.error("Copying failed, as new files are not the same length: " + fromFile.length() + " / " - + toFile.length()); + FileHelper.logger.error("Copying failed, as new files are not the same length: " + fromFile.length() + + " / " + toFile.length()); toFile.delete(); return false; @@ -213,7 +215,7 @@ public class FileHelper { } catch (Exception e) { - FileHelper.logger.error(e, e); + FileHelper.logger.error(e.getMessage(), e); return false; } finally { diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java b/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java deleted file mode 100644 index 678ff78f3..000000000 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jConfigurator.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright (c) 2012 - * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . - * - */ -package ch.eitchnet.utils.helper; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; -import java.util.Properties; - -import org.apache.log4j.BasicConfigurator; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.apache.log4j.PropertyConfigurator; - -/** - * A simple configurator to configure log4j, with fall back default configuration - * - * @author Robert von Burg - */ -public class Log4jConfigurator { - - /** - * system property used to override the log4j configuration file - */ - public static final String PROP_FILE_LOG4J = "rsp.log4j.properties"; - - /** - * default log4j configuration file - */ - public static final String FILE_LOG4J = "log4j.properties"; - - /** - * runtime log4j configuration file which is a copy of the original file but has any place holders overwritten - */ - public static final String FILE_LOG4J_TEMP = "log4j.properties.tmp"; - - private static final Logger logger = Logger.getLogger(Log4jConfigurator.class); - private static Log4jPropertyWatchDog watchDog; - - /** - * Configures log4j with the default {@link ConsoleAppender} - */ - public static synchronized void configure() { - Log4jConfigurator.cleanupOldWatchdog(); - BasicConfigurator.resetConfiguration(); - BasicConfigurator.configure(new ConsoleAppender(Log4jConfigurator.getDefaulLayout())); - Logger.getRootLogger().setLevel(Level.INFO); - } - - /** - * Returns the default layout: %d %5p [%t] %C{1} %M - %m%n - * - * @return the default layout - */ - public static PatternLayout getDefaulLayout() { - return new PatternLayout("%d %5p [%t] %C{1} %M - %m%n"); - } - - /** - *

- * Loads the log4j configuration - *

- * - *

- * This file is configurable through the {@link Log4jConfigurator#PROP_FILE_LOG4J} system property, but uses the - * default {@link Log4jConfigurator#FILE_LOG4J} file, if no configuration option is set. The path used is - * /config/ whereas is a system property - *

- * - *

- * Any properties in the properties are substituted using - * {@link StringHelper#replaceProperties(Properties, Properties)} and then the configuration file is written to a - * new file /tmp/ {@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally - * {@link PropertyConfigurator#configureAndWatch(String)} is called so that the configuration is loaded and log4j - * watches the temporary file for configuration changes - *

- */ - public static synchronized void loadLog4jConfiguration() { - - // get a configured log4j properties file, or use default - // RSPConfigConstants.FILE_LOG4J - String fileLog4j = SystemHelper.getProperty(Log4jConfigurator.class.getName(), - Log4jConfigurator.PROP_FILE_LOG4J, Log4jConfigurator.FILE_LOG4J); - - // get the root directory - String userDir = System.getProperty("user.dir"); - String configDir = userDir + "/config/"; - - String pathNameToLog4j = configDir + fileLog4j; - File log4JPath = new File(pathNameToLog4j); - - try { - - // load the log4j.properties file - if (!log4JPath.exists()) - throw new RuntimeException("The log4j configuration file does not exist at " - + log4JPath.getAbsolutePath()); - - // now perform the loading - Log4jConfigurator.loadLog4jConfiguration(log4JPath); - - } catch (Exception e) { - - Log4jConfigurator.configure(); - Log4jConfigurator.logger.error(e, e); - Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file exists at " - + log4JPath.getAbsolutePath()); - - } - } - - /** - *

- * Loads the given log4j configuration - *

- * - *

- * Any properties in the properties are substituted using - * {@link StringHelper#replaceProperties(Properties, Properties)} and then the configuration file is written to a - * new file /tmp/ {@link Log4jConfigurator#FILE_LOG4J_TEMP} and then finally - * {@link PropertyConfigurator#configureAndWatch(String)} is called so that the configuration is loaded and log4j - * watches the temporary file for configuration changes - *

- * - * @param log4jConfigPath - */ - public static synchronized void loadLog4jConfiguration(File log4jConfigPath) { - - if (log4jConfigPath == null) - throw new RuntimeException("log4jConfigPath may not be null!"); - - // first clean up any old watch dog in case of a programmatic re-load of the configuration - Log4jConfigurator.cleanupOldWatchdog(); - - // get the root directory - String userDir = System.getProperty("user.dir"); - String tmpDir = userDir + "/tmp/"; - - String pathNameToLog4jTemp = tmpDir + Log4jConfigurator.FILE_LOG4J_TEMP; - Properties log4jProperties = new Properties(); - FileInputStream fin = null; - FileOutputStream fout = null; - - try { - - fin = new FileInputStream(log4jConfigPath); - log4jProperties.load(fin); - fin.close(); - - // replace any variables - StringHelper.replaceProperties(log4jProperties, System.getProperties()); - - // write this as the temporary log4j file - File logsFileDir = new File(tmpDir); - if (!logsFileDir.exists() && !logsFileDir.mkdirs()) - throw new RuntimeException("Could not create log path " + logsFileDir.getAbsolutePath()); - - fout = new FileOutputStream(pathNameToLog4jTemp); - log4jProperties.store(fout, "Running instance log4j configuration " + new Date()); - fout.close(); - - // XXX if the server is in a web context, then we may not use the - // FileWatchDog - BasicConfigurator.resetConfiguration(); - Log4jConfigurator.watchDog = new Log4jPropertyWatchDog(pathNameToLog4jTemp); - Log4jConfigurator.watchDog.start(); - - Log4jConfigurator.logger.info("Log4j is configured to use and watch file " + pathNameToLog4jTemp); - - } catch (Exception e) { - - Log4jConfigurator.configure(); - Log4jConfigurator.logger.error(e, e); - Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check the log4j configuration file at " - + log4jConfigPath); - - } finally { - if (fin != null) { - try { - fin.close(); - } catch (IOException e) { - Log4jConfigurator.logger.error("Exception closing input file: " + e, e); - } - } - if (fout != null) { - try { - fout.close(); - } catch (IOException e) { - Log4jConfigurator.logger.error("Exception closing output file: " + e, e); - } - } - } - } - - /** - *

- * Loads the log4j configuration file as a class resource by calling {@link Class#getResourceAsStream(String)} for - * the given class - *

- * - * @param clazz - */ - public static synchronized void loadLog4jConfigurationAsResource(Class clazz) { - - try { - - if (clazz == null) - throw new RuntimeException("clazz may not be null!"); - - InputStream resourceAsStream = clazz.getResourceAsStream("/" + Log4jConfigurator.FILE_LOG4J); - if (resourceAsStream == null) { - throw new RuntimeException("The resource '" + Log4jConfigurator.FILE_LOG4J + "' could not be found for class " - + clazz.getName()); - } - - // load the properties from the input stream - Properties log4jProperties = new Properties(); - log4jProperties.load(resourceAsStream); - - // and then - Log4jConfigurator.loadLog4jConfiguration(log4jProperties); - - } catch (Exception e) { - Log4jConfigurator.configure(); - Log4jConfigurator.logger.error(e, e); - Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. Please check that the log4j configuration file '" - + Log4jConfigurator.FILE_LOG4J + "' exists as a resource for class " + clazz.getName() - + " and is a valid properties configuration"); - } - } - - /** - *

- * Loads the given log4j configuration. Log4j is configured with the given properties. The only change is that - * {@link StringHelper#replaceProperties(Properties, Properties)} is used to replace any properties - *

- * - *

- * No property watch dog is loaded - *

- * - * @param log4jProperties - * the properties to use for the log4j configuration - */ - public static synchronized void loadLog4jConfiguration(Properties log4jProperties) { - - try { - - if (log4jProperties == null) - throw new RuntimeException("log4jProperties may not be null!"); - - // first clean up any old watch dog in case of a programmatic re-load of the configuration - Log4jConfigurator.cleanupOldWatchdog(); - - // replace any variables - StringHelper.replaceProperties(log4jProperties, System.getProperties()); - - // now configure log4j - PropertyConfigurator.configure(log4jProperties); - Log4jConfigurator.logger.info("Log4j is configured using the given properties."); - - } catch (Exception e) { - Log4jConfigurator.configure(); - Log4jConfigurator.logger.error(e, e); - Log4jConfigurator.logger.error("Log4j COULD NOT BE INITIALIZED. The given log4jProperties seem not to be valid!"); - } - } - - /** - * Cleanup a running watch dog - */ - public static synchronized void cleanupOldWatchdog() { - // clean up an old watch dog - if (Log4jConfigurator.watchDog != null) { - Log4jConfigurator.logger.info("Stopping old Log4j watchdog."); - Log4jConfigurator.watchDog.interrupt(); - try { - Log4jConfigurator.watchDog.join(1000l); - } catch (InterruptedException e) { - Log4jConfigurator.logger.error("Oops. Could not terminate an old WatchDog."); - } finally { - Log4jConfigurator.watchDog = null; - } - Log4jConfigurator.logger.info("Done."); - } - } -} diff --git a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java b/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java deleted file mode 100644 index e13794550..000000000 --- a/src/main/java/ch/eitchnet/utils/helper/Log4jPropertyWatchDog.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2012 - * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . - * - */ -package ch.eitchnet.utils.helper; - -import java.io.File; - -import org.apache.log4j.LogManager; -import org.apache.log4j.PropertyConfigurator; -import org.apache.log4j.helpers.LogLog; - -/** - * @author Robert von Burg - * - */ -public class Log4jPropertyWatchDog extends Thread { - - /** - * The default delay between every file modification check, set to 60 seconds. - */ - public static final long DEFAULT_DELAY = 60000; - - /** - * The name of the file to observe for changes. - */ - protected String filename; - - /** - * The delay to observe between every check. By default set {@link #DEFAULT_DELAY}. - */ - protected long delay = Log4jPropertyWatchDog.DEFAULT_DELAY; - - protected File file; - protected long lastModif = 0; - protected boolean warnedAlready = false; - protected boolean interrupted = false; - - /** - * @param filename - */ - protected Log4jPropertyWatchDog(String filename) { - super("FileWatchdog"); - this.filename = filename; - this.file = new File(filename); - setDaemon(true); - checkAndConfigure(); - } - - /** - * Set the delay to observe between each check of the file changes. - */ - public void setDelay(long delay) { - this.delay = delay; - } - - /** - * - */ - protected void checkAndConfigure() { - boolean fileExists; - try { - fileExists = this.file.exists(); - } catch (SecurityException e) { - LogLog.warn("Was not allowed to read check file existance, file:[" + this.filename + "]."); - this.interrupted = true; // there is no point in continuing - return; - } - - if (fileExists) { - long l = this.file.lastModified(); // this can also throw a SecurityException - if (l > this.lastModif) { // however, if we reached this point this - this.lastModif = l; // is very unlikely. - doOnChange(); - this.warnedAlready = false; - } - } else { - if (!this.warnedAlready) { - LogLog.debug("[" + this.filename + "] does not exist."); - this.warnedAlready = true; - } - } - } - - /** - * Call {@link PropertyConfigurator#configure(String)} with the filename to reconfigure log4j. - */ - public void doOnChange() { - PropertyConfigurator propertyConfigurator = new PropertyConfigurator(); - propertyConfigurator.doConfigure(this.filename, LogManager.getLoggerRepository()); - } - - /** - * @see java.lang.Thread#run() - */ - @Override - public void run() { - while (!this.interrupted) { - try { - Thread.sleep(this.delay); - } catch (InterruptedException e) { - // no interruption expected - this.interrupted = true; - } - checkAndConfigure(); - } - } -} diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index c30366426..b2c974b48 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -24,14 +24,15 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Robert von Burg */ public class ProcessHelper { - private static final Logger logger = Logger.getLogger(ProcessHelper.class); + private static final Logger logger = LoggerFactory.getLogger(ProcessHelper.class); public static ProcessResult runCommand(String command) { final StringBuffer sb = new StringBuffer(); @@ -40,8 +41,7 @@ public class ProcessHelper { final Process process = Runtime.getRuntime().exec(command); - final BufferedReader errorStream = new BufferedReader( - new InputStreamReader(process.getErrorStream())); + final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); Thread errorIn = new Thread("errorIn") { @Override public void run() { @@ -50,8 +50,7 @@ public class ProcessHelper { }; errorIn.start(); - final BufferedReader inputStream = new BufferedReader( - new InputStreamReader(process.getInputStream())); + final BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream())); Thread infoIn = new Thread("infoIn") { @Override public void run() { @@ -69,8 +68,7 @@ public class ProcessHelper { return new ProcessResult(returnValue, sb.toString(), null); } catch (IOException e) { - throw new RuntimeException("Failed to perform command: " - + e.getLocalizedMessage(), e); + throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); } catch (InterruptedException e) { ProcessHelper.logger.error("Interrupted!"); sb.append("[FATAL] Interrupted"); @@ -78,12 +76,10 @@ public class ProcessHelper { } } - public static ProcessResult runCommand(File workingDirectory, - String... commandAndArgs) { + public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { if (!workingDirectory.exists()) - throw new RuntimeException("Working directory does not exist at " - + workingDirectory.getAbsolutePath()); + throw new RuntimeException("Working directory does not exist at " + workingDirectory.getAbsolutePath()); if (commandAndArgs == null || commandAndArgs.length == 0) throw new RuntimeException("No command passed!"); @@ -97,8 +93,7 @@ public class ProcessHelper { final Process process = processBuilder.start(); - final BufferedReader errorStream = new BufferedReader( - new InputStreamReader(process.getErrorStream())); + final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); Thread errorIn = new Thread("errorIn") { @Override public void run() { @@ -107,8 +102,7 @@ public class ProcessHelper { }; errorIn.start(); - final BufferedReader inputStream = new BufferedReader( - new InputStreamReader(process.getInputStream())); + final BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream())); Thread infoIn = new Thread("infoIn") { @Override public void run() { @@ -126,8 +120,7 @@ public class ProcessHelper { return new ProcessResult(returnValue, sb.toString(), null); } catch (IOException e) { - throw new RuntimeException("Failed to perform command: " - + e.getLocalizedMessage(), e); + throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); } catch (InterruptedException e) { ProcessHelper.logger.error("Interrupted!"); sb.append("[FATAL] Interrupted"); @@ -147,16 +140,14 @@ public class ProcessHelper { } } - private static void readStream(StringBuffer sb, String prefix, - BufferedReader bufferedReader) { + private static void readStream(StringBuffer sb, String prefix, BufferedReader bufferedReader) { String line; try { while ((line = bufferedReader.readLine()) != null) { sb.append(prefix + line + "\n"); } } catch (IOException e) { - String msg = "Faild to read from " + prefix + " stream: " - + e.getLocalizedMessage(); + String msg = "Faild to read from " + prefix + " stream: " + e.getLocalizedMessage(); sb.append("[FATAL] " + msg + "\n"); } } @@ -173,11 +164,9 @@ public class ProcessHelper { String pdfFile = pdfPath.getAbsolutePath(); if (pdfFile.charAt(0) == '/') pdfFile = pdfFile.substring(1); - processResult = ProcessHelper.runCommand("rundll32 url.dll,FileProtocolHandler " - + pdfFile); + processResult = ProcessHelper.runCommand("rundll32 url.dll,FileProtocolHandler " + pdfFile); } else { - throw new UnsupportedOperationException("Unexpected OS: " - + SystemHelper.osName); + throw new UnsupportedOperationException("Unexpected OS: " + SystemHelper.osName); } ProcessHelper.logProcessResult(processResult); @@ -187,14 +176,11 @@ public class ProcessHelper { if (processResult.returnValue == 0) { ProcessHelper.logger.info("Process executed successfully"); } else if (processResult.returnValue == -1) { - ProcessHelper.logger.error("Process execution failed:\n" - + processResult.processOutput); - ProcessHelper.logger.error(processResult.t, processResult.t); + ProcessHelper.logger.error("Process execution failed:\n" + processResult.processOutput); + ProcessHelper.logger.error(processResult.t.getMessage(), processResult.t); } else { ProcessHelper.logger.info("Process execution was not successful with return value:" - + processResult.returnValue - + "\n" - + processResult.processOutput); + + processResult.returnValue + "\n" + processResult.processOutput); } } } diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 9ba4b56fb..d64cbee6a 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -1,465 +1,466 @@ -/* - * Copyright (c) 2012 - * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . - * - */ -package ch.eitchnet.utils.helper; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Properties; - -import org.apache.log4j.Logger; - -/** - * A helper class to perform different actions on {@link String}s - * - * @author Robert von Burg - */ -public class StringHelper { - - private static final Logger logger = Logger.getLogger(StringHelper.class); - - /** - * Hex char table for fast calculating of hex value - */ - 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++] = StringHelper.HEX_CHAR_TABLE[v >>> 4]; - hex[index++] = StringHelper.HEX_CHAR_TABLE[v & 0xF]; - } - - return new String(hex, "ASCII"); - - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Something went wrong while converting to HEX: " + e.getLocalizedMessage(), 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."); - - 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 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 StringHelper.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 StringHelper.hash("MD5", bytes); - } - - /** - * 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 StringHelper.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 StringHelper.hash("SHA-1", bytes); - } - - /** - * 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 StringHelper.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 StringHelper.hash("SHA-256", 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) { - throw new RuntimeException("Algorithm " + algorithm + " does not exist!", 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 StringHelper.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) { - - StringHelper.logger.warn("Shortening length of value: " + value); - StringHelper.logger.warn("Length is: " + value.length() + " max: " + length); - - 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 StringHelper.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 alue) { - - // get a copy of the value - String tmpValue = alue; - - // get first occurrence of $ character - int pos = -1; - int stop = 0; - - // loop on $ character positions - while ((pos = tmpValue.indexOf('$', 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) { - StringHelper.logger.error("Sequence starts at offset " + pos + " but does not end!"); - tmpValue = alue; - 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("$") || sequence.contains("{") || sequence.contains("}")) { - StringHelper.logger.error("Enclosed sequence in offsets " + pos + " - " + stop - + " contains one of the illegal chars: $ { }: " + sequence); - tmpValue = alue; - break; - } - - // sequence is good, so see if we have a property for it - String property = properties.getProperty(sequence, ""); - - // 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("${" + sequence + "}", property); - } - - 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) { - StringHelper.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 = StringHelper.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 = StringHelper.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 + ". Showing " + maxContext - + " extra characters and start and end:\n"); - sb.append("context s1: " + s1.substring(start, end) + "\n"); - sb.append("context s2: " + s2.substring(start, end) + "\n"); - - return sb.toString(); - } - - /** - * Formats the given number of milliseconds to a time like 0.000s/ms - * - * @param millis - * the number of milliseconds - * - * @return format the given number of milliseconds to a time like 0.000s/ms - */ - public static String formatMillisecondsDuration(final long millis) { - if (millis > 1000) { - return String.format("%.3fs", (((double) millis) / 1000)); //$NON-NLS-1$ - } - - return millis + "ms"; //$NON-NLS-1$ - } - - /** - * Formats the given number of nanoseconds to a time like 0.000s/ms/us/ns - * - * @param nanos - * the number of nanoseconds - * - * @return format the given number of nanoseconds to a time like 0.000s/ms/us/ns - */ - public static String formatNanoDuration(final long nanos) { - if (nanos > 1000000000) { - return String.format("%.3fs", (((double) nanos) / 1000000000)); //$NON-NLS-1$ - } else if (nanos > 1000000) { - return String.format("%.3fms", (((double) nanos) / 1000000)); //$NON-NLS-1$ - } else if (nanos > 1000) { - return String.format("%.3fus", (((double) nanos) / 1000)); //$NON-NLS-1$ - } else { - return nanos + "ns"; //$NON-NLS-1$ - } - } - - /** - * 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(); - } -} +/* + * Copyright (c) 2012 + * + * This file is part of ch.eitchnet.java.utils + * + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ch.eitchnet.java.utils. If not, see . + * + */ +package ch.eitchnet.utils.helper; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +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 { + + private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); + + /** + * Hex char table for fast calculating of hex value + */ + 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++] = StringHelper.HEX_CHAR_TABLE[v >>> 4]; + hex[index++] = StringHelper.HEX_CHAR_TABLE[v & 0xF]; + } + + return new String(hex, "ASCII"); + + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Something went wrong while converting to HEX: " + e.getLocalizedMessage(), 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."); + + 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 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 StringHelper.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 StringHelper.hash("MD5", bytes); + } + + /** + * 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 StringHelper.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 StringHelper.hash("SHA-1", bytes); + } + + /** + * 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 StringHelper.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 StringHelper.hash("SHA-256", 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) { + throw new RuntimeException("Algorithm " + algorithm + " does not exist!", 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 StringHelper.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) { + + StringHelper.logger.warn("Shortening length of value: " + value); + StringHelper.logger.warn("Length is: " + value.length() + " max: " + length); + + 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 StringHelper.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 alue) { + + // get a copy of the value + String tmpValue = alue; + + // get first occurrence of $ character + int pos = -1; + int stop = 0; + + // loop on $ character positions + while ((pos = tmpValue.indexOf('$', 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) { + StringHelper.logger.error("Sequence starts at offset " + pos + " but does not end!"); + tmpValue = alue; + 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("$") || sequence.contains("{") || sequence.contains("}")) { + StringHelper.logger.error("Enclosed sequence in offsets " + pos + " - " + stop + + " contains one of the illegal chars: $ { }: " + sequence); + tmpValue = alue; + break; + } + + // sequence is good, so see if we have a property for it + String property = properties.getProperty(sequence, ""); + + // 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("${" + sequence + "}", property); + } + + 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) { + StringHelper.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 = StringHelper.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 = StringHelper.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 + ". Showing " + maxContext + + " extra characters and start and end:\n"); + sb.append("context s1: " + s1.substring(start, end) + "\n"); + sb.append("context s2: " + s2.substring(start, end) + "\n"); + + return sb.toString(); + } + + /** + * Formats the given number of milliseconds to a time like 0.000s/ms + * + * @param millis + * the number of milliseconds + * + * @return format the given number of milliseconds to a time like 0.000s/ms + */ + public static String formatMillisecondsDuration(final long millis) { + if (millis > 1000) { + return String.format("%.3fs", (((double) millis) / 1000)); //$NON-NLS-1$ + } + + return millis + "ms"; //$NON-NLS-1$ + } + + /** + * Formats the given number of nanoseconds to a time like 0.000s/ms/us/ns + * + * @param nanos + * the number of nanoseconds + * + * @return format the given number of nanoseconds to a time like 0.000s/ms/us/ns + */ + public static String formatNanoDuration(final long nanos) { + if (nanos > 1000000000) { + return String.format("%.3fs", (((double) nanos) / 1000000000)); //$NON-NLS-1$ + } else if (nanos > 1000000) { + return String.format("%.3fms", (((double) nanos) / 1000000)); //$NON-NLS-1$ + } else if (nanos > 1000) { + return String.format("%.3fus", (((double) nanos) / 1000)); //$NON-NLS-1$ + } else { + return nanos + "ns"; //$NON-NLS-1$ + } + } + + /** + * 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(); + } +} diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index 50fb91628..fbf925bca 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -19,7 +19,6 @@ */ package ch.eitchnet.utils.helper; - /** * A helper class for {@link System} methods * @@ -74,7 +73,7 @@ public class SystemHelper { sb.append(SystemHelper.javaVersion); return sb.toString(); } - + public static String getUserDir() { return System.getProperty("user.dir"); } @@ -92,7 +91,8 @@ public class SystemHelper { } public static boolean is32bit() { - return SystemHelper.osArch.equals("x86") || SystemHelper.osArch.equals("i386") || SystemHelper.osArch.equals("i686"); + return SystemHelper.osArch.equals("x86") || SystemHelper.osArch.equals("i386") + || SystemHelper.osArch.equals("i686"); } public static boolean is64bit() { @@ -112,7 +112,8 @@ public class SystemHelper { } public static String getMemorySummary() { - return "Memory available " + SystemHelper.getMaxMemory() + " / Used: " + SystemHelper.getUsedMemory() + " / Free:" + SystemHelper.getFreeMemory(); + return "Memory available " + SystemHelper.getMaxMemory() + " / Used: " + SystemHelper.getUsedMemory() + + " / Free:" + SystemHelper.getFreeMemory(); } /** diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index 2c55d2705..57e6a0132 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -19,7 +19,8 @@ */ package ch.eitchnet.utils.objectfilter; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This class is a cache for objects whose operations (additions, modifications, removals) are first collected and then @@ -40,7 +41,7 @@ import org.apache.log4j.Logger; */ public class ObjectCache { - private final static Logger logger = Logger.getLogger(ObjectCache.class); + private final static Logger logger = LoggerFactory.getLogger(ObjectCache.class); /** * id The unique ID of this object in this session @@ -72,8 +73,8 @@ public class ObjectCache { this.operation = operation; if (ObjectCache.logger.isDebugEnabled()) { - ObjectCache.logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + " / " - + object.toString()); + ObjectCache.logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + + " / " + object.toString()); } } @@ -96,7 +97,8 @@ public class ObjectCache { */ public void setOperation(Operation newOperation) { if (ObjectCache.logger.isDebugEnabled()) { - ObjectCache.logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " + newOperation); + ObjectCache.logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " + + newOperation); } this.operation = newOperation; } diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index 51733db77..f7b93124b 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -26,7 +26,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * This class implements a filter where modifications to an object are collected, and only the most recent action and @@ -79,10 +80,10 @@ import org.apache.log4j.Logger; * @param */ public class ObjectFilter { - + // XXX think about removing the generic T, as there is no sense in it - private final static Logger logger = Logger.getLogger(ObjectFilter.class); + private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class); private HashMap> cache = new HashMap>(); private HashSet keySet = new HashSet(); From 5e56bf28d4d9c56a427dc75751a56f5c9e0ac125 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 25 Nov 2012 00:45:47 +0100 Subject: [PATCH 013/180] [Minor] Removed dependency to ch.eitchnet.log4j and thus added log4j.xml The log4j.xml configuration file is in the test resources and now the project is completely free of a dependency to a concrete logging implementation as all logging of the sources is done over slf4j --- src/test/resources/log4j.xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/test/resources/log4j.xml diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml new file mode 100644 index 000000000..a35a3c351 --- /dev/null +++ b/src/test/resources/log4j.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5fcea2bb77778c131f483a22c1d3cf177755e370 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 20 Jan 2013 23:17:14 +0100 Subject: [PATCH 014/180] [New] added StringHelper.parseBoolean() to check if the string value passed is either false or true, not allowing other string values. --- .../eitchnet/utils/helper/StringHelper.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index d64cbee6a..a0f3e2b95 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -463,4 +463,36 @@ public class StringHelper { public static boolean isEmpty(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"); + String tmp = value.toLowerCase(); + if (tmp.equals(Boolean.TRUE.toString())) { + return true; + } else if (tmp.equals(Boolean.FALSE.toString())) { + return false; + } else { + throw new RuntimeException("Value " + value + + " can not be parsed to boolean! Expected case insensitive true or false"); + } + } } From 81a76a5fe0cb021e296db86c187f5f51eb4da68c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 20 Jan 2013 23:20:36 +0100 Subject: [PATCH 015/180] [New] Added XmlHelper --- .../utils/exceptions/XmlException.java | 45 +++++ .../ch/eitchnet/utils/helper/XmlHelper.java | 175 ++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/exceptions/XmlException.java create mode 100644 src/main/java/ch/eitchnet/utils/helper/XmlHelper.java diff --git a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java new file mode 100644 index 000000000..a453dbc86 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java @@ -0,0 +1,45 @@ +/* + * 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.utils.exceptions; + +/** + * @author Robert von Burg + * + */ +public class XmlException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * @param message + */ + public XmlException(String message) { + super(message); + } + + /** + * @param message + * @param cause + */ + public XmlException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java new file mode 100644 index 000000000..d8c61bb24 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2010 - 2012 + * + * This file is part of ch.eitchnet.java.utils. + * + * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ch.eitchnet.java.utils is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ch.eitchnet.java.utils. If not, see . + * + */ +package ch.eitchnet.utils.helper; + +import java.io.File; +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import ch.eitchnet.utils.exceptions.XmlException; + +/** + * Helper class for performing XML based tasks using Dom4J + * + * @author Robert von Burg + */ +public class XmlHelper { + + /** + * DEFAULT_ENCODING = "UTF-8" : defines the default UTF-8 encoding expected of XML files + */ + public static final String DEFAULT_ENCODING = "UTF-8"; + + private static final Logger logger = LoggerFactory.getLogger(XmlHelper.class); + + /** + * Parses an XML file on the file system using dom4j and returns the resulting {@link Document} object + * + * @param xmlFile + * the {@link File} which has the path to the XML file to read + * + * @return a {@link Document} object containing the dom4j {@link Element}s of the XML file + */ + public static void parseDocument(File xmlFile, DefaultHandler xmlHandler) { + + try { + + SAXParserFactory spf = SAXParserFactory.newInstance(); + + SAXParser sp = spf.newSAXParser(); + XmlHelper.logger.info("Parsing XML document " + xmlFile.getAbsolutePath()); + sp.parse(xmlFile, xmlHandler); + + } catch (ParserConfigurationException e) { + throw new XmlException("Failed to initialize a SAX Parser: " + e.getLocalizedMessage(), e); + } catch (SAXException e) { + throw new XmlException("The XML file " + xmlFile.getAbsolutePath() + " is not parseable:", e); + } catch (IOException e) { + throw new XmlException("The XML could not be read: " + xmlFile.getAbsolutePath()); + } + } + + /** + * Writes a dom4j {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param file + * the {@link File} describing the path on the file system where the XML file should be written to + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeDocument(Document document, File file) throws RuntimeException { + + XmlHelper.logger.info("Exporting document element " + document.getNodeName() + " to " + file.getAbsolutePath()); + + try { + + String encoding = document.getInputEncoding(); + if (encoding == null || encoding.isEmpty()) { + encoding = XmlHelper.DEFAULT_ENCODING; + } + + // Set up a transformer + TransformerFactory transfac = TransformerFactory.newInstance(); + Transformer transformer = transfac.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.ENCODING, encoding); + transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); + //transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", "\t"); + + // Transform to file + StreamResult result = new StreamResult(file); + Source xmlSource = new DOMSource(document); + transformer.transform(xmlSource, result); + + } catch (Exception e) { + + throw new XmlException("Exception while exporting to file: " + e, e); + + } + } + + /** + * Writes a dom4j {@link Element} to an XML file on the file system + * + * @param rootElement + * the {@link Element} to write to the file system + * @param file + * the {@link File} describing the path on the file system where the XML file should be written to + * @param encoding + * encoding to use to write the file + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeElement(Element rootElement, File file, String encoding) throws RuntimeException { + + Document document = createDocument(); + document.appendChild(rootElement); + XmlHelper.writeDocument(document, file); + } + + /** + * Returns a new document instance + * + * @return a new document instance + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration + */ + public static Document createDocument() throws RuntimeException { + try { + + DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = dbfac.newDocumentBuilder(); + Document document = docBuilder.newDocument(); + + return document; + + } catch (DOMException e) { + throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); + } catch (ParserConfigurationException e) { + throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); + } + } +} From a69946d8d4a910b9a43c38633c87b97ca820a6b5 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 30 Jan 2013 15:06:43 +0100 Subject: [PATCH 016/180] Update pom.xml Added distribution management to deploy to nexus.eitchnet.ch (fixed wrong URL) --- pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml b/pom.xml index 722434b6e..ddd624b1d 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,19 @@ codehausSnapshots Codehaus Snapshots http://snapshots.maven.codehaus.org/maven2 default ... --> + + + + deployment + Internal Releases + http://nexus.eitchnet.ch/content/repositories/releases/ + + + deployment + Internal Releases + http://nexus.eitchnet.ch/content/repositories/snapshots/ + + From ca74bf2ae855ed93ec0bd78b5140b4f9b4badba8 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 31 Jan 2013 18:57:38 +0100 Subject: [PATCH 017/180] [Minor] JavaDoc comments and file headers fixed some JavaDoc comments and file headers --- .../ch/eitchnet/utils/exceptions/XmlException.java | 8 ++++---- src/main/java/ch/eitchnet/utils/helper/XmlHelper.java | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java index a453dbc86..40dec4201 100644 --- a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java +++ b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java @@ -3,20 +3,20 @@ * * All rights reserved. * - * This file is part of the XXX. + * This file is part of the ch.eitchnet.utils. * - * XXX is free software: you can redistribute + * ch.eitchnet.utils 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 + * ch.eitchnet.utils 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 + * along with ch.eitchnet.utils. If not, see * . */ package ch.eitchnet.utils.exceptions; diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index d8c61bb24..d9a0c3af9 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -45,7 +45,7 @@ import org.xml.sax.helpers.DefaultHandler; import ch.eitchnet.utils.exceptions.XmlException; /** - * Helper class for performing XML based tasks using Dom4J + * Helper class for performing XML based tasks * * @author Robert von Burg */ @@ -59,12 +59,12 @@ public class XmlHelper { private static final Logger logger = LoggerFactory.getLogger(XmlHelper.class); /** - * Parses an XML file on the file system using dom4j and returns the resulting {@link Document} object + * Parses an XML file on the file system and returns the resulting {@link Document} object * * @param xmlFile * the {@link File} which has the path to the XML file to read * - * @return a {@link Document} object containing the dom4j {@link Element}s of the XML file + * @return a {@link Document} object containing the {@link Element}s of the XML file */ public static void parseDocument(File xmlFile, DefaultHandler xmlHandler) { @@ -86,7 +86,7 @@ public class XmlHelper { } /** - * Writes a dom4j {@link Document} to an XML file on the file system + * Writes a {@link Document} to an XML file on the file system * * @param document * the {@link Document} to write to the file system @@ -130,7 +130,7 @@ public class XmlHelper { } /** - * Writes a dom4j {@link Element} to an XML file on the file system + * Writes an {@link Element} to an XML file on the file system * * @param rootElement * the {@link Element} to write to the file system From 1cf1a16683cd27411057988adb3fcbbbc0113091 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 9 Feb 2013 15:52:25 +0100 Subject: [PATCH 018/180] [New] Added Base64, Base32 and Base16 encoding --- .../eitchnet/utils/helper/BaseEncoding.java | 373 ++++++++++++++++++ .../ch/eitchnet/utils/helper/ByteHelper.java | 255 ++++++++++++ .../utils/helper/BaseEncodingTest.java | 84 ++++ 3 files changed, 712 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java create mode 100644 src/main/java/ch/eitchnet/utils/helper/ByteHelper.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java new file mode 100644 index 000000000..3b607ed08 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2012, Robert von Burg + * + * All rights reserved. + * + * This file is part of the ch.eitchnet.java.utils. + * + * ch.eitchnet.java.utils 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. + * + * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see + * . + */ +package ch.eitchnet.utils.helper; + +/** + *
+ * + *

+ * The following implementations are supported: + *

    + *
  • Base64
  • + *
  • Base64 URL safe
  • + *
  • Base32
  • + *
  • Base32 HEX
  • + *
  • Base16 / HEX
  • + *
+ *

+ * + * @author Robert von Burg + */ +public class BaseEncoding { + + // private static final Logger logger = LoggerFactory.getLogger(BaseEncoding.class); + + private static final byte PAD = '='; + + private static final byte[] BASE_16 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', + 'F' }; + + private 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' }; + + private 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' }; + + private 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' }; + + private 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', '+', '/' }; + + private 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', '-', '_' }; + + static { + if (BASE_16.length != 16) { + throw new RuntimeException("BASE_16 alphabet does not have expected size 16 but is " + BASE_16.length); + } + if (BASE_32.length != 32) { + throw new RuntimeException("BASE_32 alphabet does not have expected size 32 but is " + BASE_32.length); + } + if (BASE_32_HEX.length != 32) { + throw new RuntimeException("BASE_32_HEX alphabet does not have expected size 32 but is " + + BASE_32_HEX.length); + } + if (BASE_32_CROCKFORD.length != 32) { + throw new RuntimeException("BASE_32_CROCKFORD alphabet does not have expected size 32 but is " + + BASE_32_CROCKFORD.length); + } + if (BASE_64.length != 64) { + throw new RuntimeException("BASE_64 alphabet does not have expected size 64 but is " + BASE_64.length); + } + if (BASE_64_SAFE.length != 64) { + throw new RuntimeException("BASE_64_SAFE alphabet does not have expected size 64 but is " + + BASE_64_SAFE.length); + } + } + + public static byte[] toBase64(byte[] bytes) { + return toBase64(BASE_64, bytes); + } + + public static byte[] toBase64Safe(byte[] bytes) { + return toBase64(BASE_64_SAFE, bytes); + } + + public static byte[] toBase32(byte[] bytes) { + return toBase32(BASE_32, bytes); + } + + public static byte[] toBase32Hex(byte[] bytes) { + return toBase32(BASE_32_HEX, bytes); + } + + public static byte[] toBase32Crockford(byte[] bytes) { + return toBase32(BASE_32_CROCKFORD, bytes); + } + + public static byte[] toBase16(byte[] bytes) { + return toBase16(bytes, BASE_16); + } + + private static byte[] toBase64(byte[] alphabet, byte[] bytes) { + if (bytes.length == 0) { + return new byte[0]; + } + + // 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 bytes * 4 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 : 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; + } + + private static byte[] toBase32(byte[] alphabet, byte[] bytes) { + if (bytes.length == 0) { + return new byte[0]; + } + + // 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; + } + + private static byte[] toBase16(byte[] bytes, byte[] alphabet) { + if (bytes.length == 0) { + return new byte[0]; + } + + // 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; + } + // 8 7 6 5 + // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32 + // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 4 3 2 1 + // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + // 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 + +} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java new file mode 100644 index 000000000..c5d5483f8 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2012, Robert von Burg + * + * All rights reserved. + * + * This file is part of the ch.eitchnet.java.utils. + * + * ch.eitchnet.java.utils 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. + * + * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see + * . + */ +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"); + + 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"); + + 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 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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(" "); + + 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/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java new file mode 100644 index 000000000..1d69c6722 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2012, Robert von Burg + * + * All rights reserved. + * + * This file is part of the ch.eitchnet.java.utils. + * + * ch.eitchnet.java.utils 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. + * + * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see + * . + */ +package ch.eitchnet.utils.helper; + +import static ch.eitchnet.utils.helper.BaseEncoding.toBase16; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase32; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase64; +import junit.framework.Assert; + +import org.junit.Test; + +/** + * @author Robert von Burg + * + */ +public class BaseEncodingTest { + + // private static final Logger logger = LoggerFactory.getLogger(BaseEncodingTest.class); + + @Test + public void testBase64() { + Assert.assertEquals("", new String(toBase64("".getBytes()))); + Assert.assertEquals("Zg==", new String(toBase64("f".getBytes()))); + Assert.assertEquals("Zm8=", new String(toBase64("fo".getBytes()))); + Assert.assertEquals("Zm9v", new String(toBase64("foo".getBytes()))); + Assert.assertEquals("Zm9vYg==", new String(toBase64("foob".getBytes()))); + Assert.assertEquals("Zm9vYmE=", new String(toBase64("fooba".getBytes()))); + Assert.assertEquals("Zm9vYmFy", new String(toBase64("foobar".getBytes()))); + } + + @Test + public void testBase32() { + Assert.assertEquals("", new String(toBase32("".getBytes()))); + Assert.assertEquals("MY======", new String(toBase32("f".getBytes()))); + Assert.assertEquals("MZXQ====", new String(toBase32("fo".getBytes()))); + Assert.assertEquals("MZXW6===", new String(toBase32("foo".getBytes()))); + Assert.assertEquals("MZXW6YQ=", new String(toBase32("foob".getBytes()))); + Assert.assertEquals("MZXW6YTB", new String(toBase32("fooba".getBytes()))); + Assert.assertEquals("MZXW6YTBOI======", new String(toBase32("foobar".getBytes()))); + } + + @Test + public void testBase32Hex() { + Assert.assertEquals("", new String(toBase32Hex("".getBytes()))); + Assert.assertEquals("CO======", new String(toBase32Hex("f".getBytes()))); + Assert.assertEquals("CPNG====", new String(toBase32Hex("fo".getBytes()))); + Assert.assertEquals("CPNMU===", new String(toBase32Hex("foo".getBytes()))); + Assert.assertEquals("CPNMUOG=", new String(toBase32Hex("foob".getBytes()))); + Assert.assertEquals("CPNMUOJ1", new String(toBase32Hex("fooba".getBytes()))); + Assert.assertEquals("CPNMUOJ1E8======", new String(toBase32Hex("foobar".getBytes()))); + } + + @Test + public void testBase16() { + Assert.assertEquals("", new String(toBase16("".getBytes()))); + Assert.assertEquals("66", new String(toBase16("f".getBytes()))); + Assert.assertEquals("666F", new String(toBase16("fo".getBytes()))); + Assert.assertEquals("666F6F", new String(toBase16("foo".getBytes()))); + Assert.assertEquals("666F6F62", new String(toBase16("foob".getBytes()))); + Assert.assertEquals("666F6F6261", new String(toBase16("fooba".getBytes()))); + Assert.assertEquals("666F6F626172", new String(toBase16("foobar".getBytes()))); + } + +} From b36ab20dc3163464bc2927ec4881b160a864adc9 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 9 Feb 2013 16:25:36 +0100 Subject: [PATCH 019/180] [New] Added Dmedia Base32 encoding --- .../eitchnet/utils/helper/BaseEncoding.java | 81 +++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 3b607ed08..71187a37d 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -55,6 +55,9 @@ public class BaseEncoding { private 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' }; + private 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' }; + private 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' }; @@ -68,30 +71,6 @@ public class BaseEncoding { '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 { - if (BASE_16.length != 16) { - throw new RuntimeException("BASE_16 alphabet does not have expected size 16 but is " + BASE_16.length); - } - if (BASE_32.length != 32) { - throw new RuntimeException("BASE_32 alphabet does not have expected size 32 but is " + BASE_32.length); - } - if (BASE_32_HEX.length != 32) { - throw new RuntimeException("BASE_32_HEX alphabet does not have expected size 32 but is " - + BASE_32_HEX.length); - } - if (BASE_32_CROCKFORD.length != 32) { - throw new RuntimeException("BASE_32_CROCKFORD alphabet does not have expected size 32 but is " - + BASE_32_CROCKFORD.length); - } - if (BASE_64.length != 64) { - throw new RuntimeException("BASE_64 alphabet does not have expected size 64 but is " + BASE_64.length); - } - if (BASE_64_SAFE.length != 64) { - throw new RuntimeException("BASE_64_SAFE alphabet does not have expected size 64 but is " - + BASE_64_SAFE.length); - } - } - public static byte[] toBase64(byte[] bytes) { return toBase64(BASE_64, bytes); } @@ -108,6 +87,10 @@ public class BaseEncoding { return toBase32(BASE_32_HEX, bytes); } + public static byte[] toBase32Dmedia(byte[] bytes) { + return toBase32(BASE_32_DMEDIA, bytes); + } + public static byte[] toBase32Crockford(byte[] bytes) { return toBase32(BASE_32_CROCKFORD, bytes); } @@ -116,10 +99,23 @@ public class BaseEncoding { return toBase16(bytes, BASE_16); } + /** + * Encodes the given data to a 64-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet + * + * @param bytes + * the bytes to encode + * @param alphabet + * the 64-bit alphabet to use + * + * @return the encoded data + */ private static byte[] toBase64(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) { return new byte[0]; } + if (alphabet.length != 64) { + throw new RuntimeException("Alphabet does not have expected size 64 but is " + alphabet.length); + } // 6 bits input for every 8 bits (1 byte) output // least common multiple of 6 bits input and 8 bits output = 24 @@ -213,10 +209,23 @@ public class BaseEncoding { return txt; } - private static byte[] toBase32(byte[] alphabet, byte[] bytes) { + /** + * Encodes the given data to a 32-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet + * + * @param bytes + * the bytes to encode + * @param alphabet + * the 32-bit alphabet to use + * + * @return the encoded data + */ + public static byte[] toBase32(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) { return new byte[0]; } + if (alphabet.length != 32) { + throw new RuntimeException("Alphabet does not have expected size 32 but is " + alphabet.length); + } // 5 bits input for every 8 bits (1 byte) output // least common multiple of 5 bits input and 8 bits output = 40 @@ -324,10 +333,23 @@ public class BaseEncoding { return txt; } - private static byte[] toBase16(byte[] bytes, byte[] alphabet) { + /** + * Encodes the given data to a 16-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet + * + * @param bytes + * the bytes to encode + * @param alphabet + * the 16-bit alphabet to use + * + * @return the encoded data + */ + public static byte[] toBase16(byte[] bytes, byte[] alphabet) { if (bytes.length == 0) { return new byte[0]; } + if (alphabet.length != 32) { + throw new RuntimeException("Alphabet does not have expected size 32 but is " + alphabet.length); + } // calculate output text length int nrOfInputBytes = bytes.length; @@ -363,11 +385,4 @@ public class BaseEncoding { return txt; } - // 8 7 6 5 - // 63 62 61 60 59 58 57 56 55 54 53 52 51 50 49 48 47 46 45 44 43 42 41 40 39 38 37 36 35 34 33 32 - // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - // 4 3 2 1 - // 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 - // 0 0 0 0 0 0 0 1 0 0 0 0 1 1 0 1 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 - } \ No newline at end of file From 4d61872094581ec6e892d3f7f63428effb878b9c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 10 Feb 2013 18:38:40 +0100 Subject: [PATCH 020/180] fixed failing junit tests --- .../eitchnet/utils/helper/BaseEncoding.java | 71 +++++++++---------- .../utils/helper/BaseEncodingTest.java | 1 - 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 71187a37d..8ed687540 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -46,30 +46,29 @@ public class BaseEncoding { private static final byte PAD = '='; - private 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_16 = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - private 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 = { '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' }; - private 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_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' }; - private 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_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' }; - private 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_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' }; - private static final byte[] BASE_64 = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 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', '+', '/' }; - - private 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', '-', '_' }; + '5', '6', '7', '8', '9', '-', '_' }; public static byte[] toBase64(byte[] bytes) { return toBase64(BASE_64, bytes); @@ -96,26 +95,24 @@ public class BaseEncoding { } public static byte[] toBase16(byte[] bytes) { - return toBase16(bytes, BASE_16); + return toBase16(BASE_16, bytes); } /** * Encodes the given data to a 64-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet * - * @param bytes - * the bytes to encode * @param alphabet * the 64-bit alphabet to use + * @param bytes + * the bytes to encode * * @return the encoded data */ - private static byte[] toBase64(byte[] alphabet, byte[] bytes) { - if (bytes.length == 0) { + public static byte[] toBase64(byte[] alphabet, byte[] bytes) { + if (bytes.length == 0) return new byte[0]; - } - if (alphabet.length != 64) { + if (alphabet.length != 64) throw new RuntimeException("Alphabet does not have expected size 64 but is " + alphabet.length); - } // 6 bits input for every 8 bits (1 byte) output // least common multiple of 6 bits input and 8 bits output = 24 @@ -212,20 +209,18 @@ public class BaseEncoding { /** * Encodes the given data to a 32-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet * - * @param bytes - * the bytes to encode * @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) { + if (bytes.length == 0) return new byte[0]; - } - if (alphabet.length != 32) { + if (alphabet.length != 32) throw new RuntimeException("Alphabet does not have expected size 32 but is " + alphabet.length); - } // 5 bits input for every 8 bits (1 byte) output // least common multiple of 5 bits input and 8 bits output = 40 @@ -336,20 +331,18 @@ public class BaseEncoding { /** * Encodes the given data to a 16-bit alphabet encoding. Thus the passed alphabet can be any arbitrary alphabet * - * @param bytes - * the bytes to encode * @param alphabet * the 16-bit alphabet to use + * @param bytes + * the bytes to encode * * @return the encoded data */ - public static byte[] toBase16(byte[] bytes, byte[] alphabet) { - if (bytes.length == 0) { + public static byte[] toBase16( byte[] alphabet, byte[] bytes) { + if (bytes.length == 0) return new byte[0]; - } - if (alphabet.length != 32) { - throw new RuntimeException("Alphabet does not have expected size 32 but is " + alphabet.length); - } + if (alphabet.length != 16) + throw new RuntimeException("Alphabet does not have expected size 16 but is " + alphabet.length); // calculate output text length int nrOfInputBytes = bytes.length; diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index 1d69c6722..d6c85ef13 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -80,5 +80,4 @@ public class BaseEncodingTest { Assert.assertEquals("666F6F6261", new String(toBase16("fooba".getBytes()))); Assert.assertEquals("666F6F626172", new String(toBase16("foobar".getBytes()))); } - } From f04de1e935ee2dc4248d278816e4edcc9ddee225 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 10 Feb 2013 18:38:59 +0100 Subject: [PATCH 021/180] Minor code cleanup --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index a0f3e2b95..bbd6def33 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -39,9 +39,8 @@ public class StringHelper { /** * Hex char table for fast calculating of hex value */ - 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' }; + private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', + 'f' }; /** * Converts each byte of the given byte array to a HEX value and returns the concatenation of these values From 5ddb277773e3b2bdf6639c7e84d13c1dee4f455c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 24 Feb 2013 11:26:44 +0100 Subject: [PATCH 022/180] [New] Implemented RFC 4648 Base de/encoding Both encoding and decoding has been implemented. The specialty of this implementation is that it is possible to pass in your own alphabet thus allowing an extension without a re-implementation (again). As an addition the dbase32 alphabet was added. See http://docs.novacut.com/dbase32/dbase32.html for usage details --- .../eitchnet/utils/helper/BaseDecoding.java | 483 ++++++++++++++++++ .../eitchnet/utils/helper/BaseEncoding.java | 2 +- .../ch/eitchnet/utils/helper/ByteHelper.java | 19 + .../utils/helper/BaseDecodingTest.java | 128 +++++ .../utils/helper/BaseEncodingTest.java | 36 +- .../GenerateReverseBaseEncodingAlphabets.java | 76 +++ 6 files changed, 742 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java new file mode 100644 index 000000000..412486a36 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2012, Robert von Burg + * + * All rights reserved. + * + * This file is part of the ch.eitchnet.java.utils. + * + * ch.eitchnet.java.utils 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. + * + * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see + * . + */ +package ch.eitchnet.utils.helper; + +/** + *

+ * This class implements the decoding part of RFC 4648 https://tools.ietf.org/html/rfc4648. For the encoding see + * {@link BaseEncoding} + *

+ * + *

+ * All versions are implemented: Base64 with URL and file name safe encoding, Base32 with HEX and Base16 + *

+ * + * @author Robert von Burg + */ +public class BaseDecoding { + + // private static final Logger logger = LoggerFactory.getLogger(BaseDecoding.class); + + private static final byte PAD = '='; + + // these reverse base encoding alphabets were generated from the actual alphabet + + private 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 }; + private 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 }; + private 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 }; + private 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 }; + private 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 }; + private 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 }; + private 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[] fromBase64(byte[] bytes) { + return fromBase64(REV_BASE_64, bytes); + } + + public static byte[] fromBase64Safe(byte[] bytes) { + return fromBase64(REV_BASE_64_SAFE, bytes); + } + + public static byte[] fromBase32(byte[] bytes) { + return fromBase32(REV_BASE_32, bytes); + } + + public static byte[] fromBase32Hex(byte[] bytes) { + return fromBase32(REV_BASE_32_HEX, bytes); + } + + public static byte[] fromBase32Dmedia(byte[] bytes) { + return fromBase32(REV_BASE_32_DMEDIA, bytes); + } + + public static byte[] fromBase32Crockford(byte[] bytes) { + return fromBase32(REV_BASE_32_CROCKFORD, bytes); + } + + public static byte[] fromBase16(byte[] bytes) { + return fromBase16(REV_BASE_16, bytes); + } + + /** + * 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " + + (inputLength % 4)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + // 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) { + + // XXX check each byte value so that it is legal + + 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; + } + +// +// /** +// * @param a +// * @return +// */ +// private static int indexOf(byte a) { +// for (int i = 0; i < BaseEncoding.BASE_64.length; i++) { +// if (BaseEncoding.BASE_64[i] == a) +// return i; +// } +// return -1; +// } +// +// private static int indexOf1(byte a) { +// for (int i = 0; i < REV_BASE_64.length; i++) { +// if (REV_BASE_64[i] == a) +// return i; +// } +// return -1; +// } + + /** + * 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 8, but is multiple of " + + (inputLength % 8)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + // 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) { + + // XXX check each byte value so that it is legal + + 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " + + (bytes.length % 4)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + 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++]; + + if (b1 < 0) { + throw new IllegalArgumentException("Value at index " + (i - 2) + " is not in range of alphabet (0-127)" + + b1); + } + if (b2 < 0) { + throw new IllegalArgumentException("Value at index " + (i - 1) + " is not in range of alphabet (0-127)" + + b2); + } + + byte c1 = alphabet[b1]; + byte c2 = alphabet[b2]; + + if (c1 == -1) { + throw new IllegalArgumentException("Value at index " + (i - 2) + + " is referencing illegal value in alphabet: " + b1); + } + if (c2 == -1) { + throw new IllegalArgumentException("Value at index " + (i - 2) + + " is referencing illegal value in alphabet: " + b2); + } + + int dataIndex = (i / 2) - 1; + int value = ((c1 << 4) & 0xff) | c2; + data[dataIndex] = (byte) value; + } + + return data; + } +} diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 8ed687540..36e070aa6 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -172,7 +172,7 @@ public class BaseEncoding { // always start at 24. bit int bitPos = 23; - // always write 24 bits (6 bytes * 4 multiples), but this will also write into the padding + // 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) { diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java index c5d5483f8..9fc47cded 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java @@ -95,6 +95,25 @@ public class ByteHelper { return sb.toString(); } + /** + * Formats the given byte array to a binary string, separating each byte by a space + * + * @param b + * 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(" "); + } + + return sb.toString(); + } + /** * Formats the given integer to a binary string, each byte is separated by a space * diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java new file mode 100644 index 000000000..cebe427be --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -0,0 +1,128 @@ +/* + * 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.utils.helper; + +import static ch.eitchnet.utils.helper.BaseDecoding.fromBase16; +import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32; +import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32Hex; +import static ch.eitchnet.utils.helper.BaseDecoding.fromBase64; +import junit.framework.Assert; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Robert von Burg + * + */ +public class BaseDecodingTest { + private static final Logger logger = LoggerFactory.getLogger(BaseDecodingTest.class); + + @Test + public void testBase64() { + Assert.assertEquals("", new String(fromBase64("".getBytes()))); + Assert.assertEquals("f", new String(fromBase64("Zg==".getBytes()))); + Assert.assertEquals("fo", new String(fromBase64("Zm8=".getBytes()))); + Assert.assertEquals("foo", new String(fromBase64("Zm9v".getBytes()))); + Assert.assertEquals("foob", new String(fromBase64("Zm9vYg==".getBytes()))); + Assert.assertEquals("fooba", new String(fromBase64("Zm9vYmE=".getBytes()))); + Assert.assertEquals("foobar", new String(fromBase64("Zm9vYmFy".getBytes()))); + + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = 'Z'; + } + long start = System.nanoTime(); + for (int i = 0; i < 200; i++) { + fromBase64(bytes); + } + long end = System.nanoTime(); + logger.info("Decoding 200MB Base64 took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase32() { + Assert.assertEquals("", new String(fromBase32("".getBytes()))); + Assert.assertEquals("f", new String(fromBase32("MY======".getBytes()))); + Assert.assertEquals("fo", new String(fromBase32("MZXQ====".getBytes()))); + Assert.assertEquals("foo", new String(fromBase32("MZXW6===".getBytes()))); + Assert.assertEquals("foob", new String(fromBase32("MZXW6YQ=".getBytes()))); + Assert.assertEquals("fooba", new String(fromBase32("MZXW6YTB".getBytes()))); + Assert.assertEquals("foobar", new String(fromBase32("MZXW6YTBOI======".getBytes()))); + + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = 'M'; + } + long start = System.nanoTime(); + for (int i = 0; i < 200; i++) { + fromBase32(bytes); + } + long end = System.nanoTime(); + logger.info("Decoding 200MB Base32 took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase32Hex() { + Assert.assertEquals("", new String(fromBase32Hex("".getBytes()))); + Assert.assertEquals("f", new String(fromBase32Hex("CO======".getBytes()))); + Assert.assertEquals("fo", new String(fromBase32Hex("CPNG====".getBytes()))); + Assert.assertEquals("foo", new String(fromBase32Hex("CPNMU===".getBytes()))); + Assert.assertEquals("foob", new String(fromBase32Hex("CPNMUOG=".getBytes()))); + Assert.assertEquals("fooba", new String(fromBase32Hex("CPNMUOJ1".getBytes()))); + Assert.assertEquals("foobar", new String(fromBase32Hex("CPNMUOJ1E8======".getBytes()))); + + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = 'C'; + } + long start = System.nanoTime(); + for (int i = 0; i < 200; i++) { + fromBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Decoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase16() { + Assert.assertEquals("", new String(fromBase16("".getBytes()))); + Assert.assertEquals("f", new String(fromBase16("66".getBytes()))); + Assert.assertEquals("fo", new String(fromBase16("666F".getBytes()))); + Assert.assertEquals("foo", new String(fromBase16("666F6F".getBytes()))); + Assert.assertEquals("foob", new String(fromBase16("666F6F62".getBytes()))); + Assert.assertEquals("fooba", new String(fromBase16("666F6F6261".getBytes()))); + Assert.assertEquals("foobar", new String(fromBase16("666F6F626172".getBytes()))); + + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = '6'; + } + long start = System.nanoTime(); + for (int i = 0; i < 200; i++) { + fromBase16(bytes); + } + long end = System.nanoTime(); + logger.info("Decoding 200MB Base16 took " + StringHelper.formatNanoDuration(end - start)); + } +} diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index d6c85ef13..c30417014 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -28,6 +28,8 @@ import static ch.eitchnet.utils.helper.BaseEncoding.toBase64; import junit.framework.Assert; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @author Robert von Burg @@ -35,7 +37,7 @@ import org.junit.Test; */ public class BaseEncodingTest { - // private static final Logger logger = LoggerFactory.getLogger(BaseEncodingTest.class); + private static final Logger logger = LoggerFactory.getLogger(BaseEncodingTest.class); @Test public void testBase64() { @@ -46,6 +48,14 @@ public class BaseEncodingTest { Assert.assertEquals("Zm9vYg==", new String(toBase64("foob".getBytes()))); Assert.assertEquals("Zm9vYmE=", new String(toBase64("fooba".getBytes()))); Assert.assertEquals("Zm9vYmFy", new String(toBase64("foobar".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase64(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base64 took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -57,6 +67,14 @@ public class BaseEncodingTest { Assert.assertEquals("MZXW6YQ=", new String(toBase32("foob".getBytes()))); Assert.assertEquals("MZXW6YTB", new String(toBase32("fooba".getBytes()))); Assert.assertEquals("MZXW6YTBOI======", new String(toBase32("foobar".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32 took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -68,6 +86,14 @@ public class BaseEncodingTest { Assert.assertEquals("CPNMUOG=", new String(toBase32Hex("foob".getBytes()))); Assert.assertEquals("CPNMUOJ1", new String(toBase32Hex("fooba".getBytes()))); Assert.assertEquals("CPNMUOJ1E8======", new String(toBase32Hex("foobar".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -79,5 +105,13 @@ public class BaseEncodingTest { Assert.assertEquals("666F6F62", new String(toBase16("foob".getBytes()))); Assert.assertEquals("666F6F6261", new String(toBase16("fooba".getBytes()))); Assert.assertEquals("666F6F626172", new String(toBase16("foobar".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase16(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base16 took " + StringHelper.formatNanoDuration(end - start)); } } diff --git a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java new file mode 100644 index 000000000..3e22f4331 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java @@ -0,0 +1,76 @@ +/* + * 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.utils.helper; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple helper class to generate the reverse alphabets for {@link BaseDecoding} + * + * @author Robert von Burg + */ +public class GenerateReverseBaseEncodingAlphabets { + + public static void main(String[] args) { + + System.out.println(generateReverseAlphabet("REV_BASE_16", BaseEncoding.BASE_16)); + System.out.println(generateReverseAlphabet("REV_BASE_32", BaseEncoding.BASE_32)); + System.out.println(generateReverseAlphabet("REV_BASE_32_CROCKFORD", BaseEncoding.BASE_32_CROCKFORD)); + System.out.println(generateReverseAlphabet("REV_BASE_32_DMEDIA", BaseEncoding.BASE_32_DMEDIA)); + System.out.println(generateReverseAlphabet("REV_BASE_32_HEX", BaseEncoding.BASE_32_HEX)); + System.out.println(generateReverseAlphabet("REV_BASE_64", BaseEncoding.BASE_64)); + System.out.println(generateReverseAlphabet("REV_BASE_64_SAFE", BaseEncoding.BASE_64_SAFE)); + } + + public static String generateReverseAlphabet(String name, byte[] alphabet) { + + Map valueToIndex = new HashMap(); + for (byte i = 0; i < alphabet.length; i++) { + Byte value = Byte.valueOf(i); + Byte key = Byte.valueOf(alphabet[value]); + if (valueToIndex.containsKey(key)) + throw new RuntimeException("Alphabet hast twice the same value " + key + " at index " + value); + valueToIndex.put(key, value); + } + + StringBuilder sb = new StringBuilder(); + sb.append("private static final byte[] " + name + " = { "); + + Byte minusOne = Byte.valueOf((byte) -1); + for (int i = 0; i < 128; i++) { + Byte index = Byte.valueOf((byte) i); + Byte value = valueToIndex.get(index); + if (value == null) + sb.append(minusOne.toString()); + else + sb.append(value.toString()); + + if (i < 127) + sb.append(", "); + } + + sb.append(" };"); + + return sb.toString(); + } +} From 706d2413acbd6aa0099c0f5256cce100a08c9b04 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 1 Mar 2013 18:45:59 +0100 Subject: [PATCH 023/180] [New] added tests for the dmedia base32 encoding Since Jason DeRose now put up some test vectors for the dmedia base32 encoding, i thought it wise to add these in a test --- .../utils/helper/BaseDecodingTest.java | 23 +++++++++++++++++++ .../utils/helper/BaseEncodingTest.java | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java index cebe427be..0c8cf9a19 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -23,8 +23,10 @@ package ch.eitchnet.utils.helper; import static ch.eitchnet.utils.helper.BaseDecoding.fromBase16; import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32; +import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32Dmedia; import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32Hex; import static ch.eitchnet.utils.helper.BaseDecoding.fromBase64; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import junit.framework.Assert; import org.junit.Test; @@ -104,6 +106,27 @@ public class BaseDecodingTest { logger.info("Decoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); } + @Test + public void testBase32Dmedia() { + + Assert.assertEquals("", new String(fromBase32Dmedia("".getBytes()))); + Assert.assertEquals("binary foo", new String(fromBase32Dmedia("FCNPVRELI7J9FUUI".getBytes()))); + Assert.assertEquals("f", new String(fromBase32Dmedia("FR======".getBytes()))); + Assert.assertEquals("fo", new String(fromBase32Dmedia("FSQJ====".getBytes()))); + Assert.assertEquals("foo", new String(fromBase32Dmedia("FSQPX===".getBytes()))); + Assert.assertEquals("foob", new String(fromBase32Dmedia("FSQPXRJ=".getBytes()))); + Assert.assertEquals("fooba", new String(fromBase32Dmedia("FSQPXRM4".getBytes()))); + Assert.assertEquals("foobar", new String(fromBase32Dmedia("FSQPXRM4HB======".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32Dmedia took " + StringHelper.formatNanoDuration(end - start)); + } + @Test public void testBase16() { Assert.assertEquals("", new String(fromBase16("".getBytes()))); diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index c30417014..0a7042dae 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -24,6 +24,7 @@ package ch.eitchnet.utils.helper; import static ch.eitchnet.utils.helper.BaseEncoding.toBase16; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Dmedia; import static ch.eitchnet.utils.helper.BaseEncoding.toBase64; import junit.framework.Assert; @@ -95,6 +96,27 @@ public class BaseEncodingTest { long end = System.nanoTime(); logger.info("Encoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); } + + @Test + public void testBase32Dmedia() { + + Assert.assertEquals("", new String(toBase32Dmedia("".getBytes()))); + Assert.assertEquals("FCNPVRELI7J9FUUI", new String(toBase32Dmedia("binary foo".getBytes()))); + Assert.assertEquals("FR======", new String(toBase32Dmedia("f".getBytes()))); + Assert.assertEquals("FSQJ====", new String(toBase32Dmedia("fo".getBytes()))); + Assert.assertEquals("FSQPX===", new String(toBase32Dmedia("foo".getBytes()))); + Assert.assertEquals("FSQPXRJ=", new String(toBase32Dmedia("foob".getBytes()))); + Assert.assertEquals("FSQPXRM4", new String(toBase32Dmedia("fooba".getBytes()))); + Assert.assertEquals("FSQPXRM4HB======", new String(toBase32Dmedia("foobar".getBytes()))); + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32Dmedia took " + StringHelper.formatNanoDuration(end - start)); + } @Test public void testBase16() { From 85bab370a57a8b9d8ac33f768dc82a69e964433b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 10 Mar 2013 22:09:43 +0100 Subject: [PATCH 024/180] [Major] Refactored the base encoding Merged the BaseEncoding and BaseDecoding classes as it seems a better fit. Added isBaseEncoded methos for checking --- .../eitchnet/utils/helper/ArraysHelper.java | 38 ++ .../eitchnet/utils/helper/BaseDecoding.java | 483 -------------- .../eitchnet/utils/helper/BaseEncoding.java | 621 +++++++++++++++++- .../utils/helper/BaseDecodingTest.java | 82 +-- .../utils/helper/BaseEncodingTest.java | 76 +-- 5 files changed, 734 insertions(+), 566 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java delete mode 100644 src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java new file mode 100644 index 000000000..c54ac3e58 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java @@ -0,0 +1,38 @@ +/* + * 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.utils.helper; + +/** + * @author Robert von Burg + * + */ +public class ArraysHelper { + + public static boolean contains(byte[] bytes, byte searchByte) { + for (byte b : bytes) { + if (b == searchByte) + return true; + } + + return false; + } +} diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java deleted file mode 100644 index 412486a36..000000000 --- a/src/main/java/ch/eitchnet/utils/helper/BaseDecoding.java +++ /dev/null @@ -1,483 +0,0 @@ -/* - * Copyright (c) 2012, Robert von Burg - * - * All rights reserved. - * - * This file is part of the ch.eitchnet.java.utils. - * - * ch.eitchnet.java.utils 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. - * - * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see - * . - */ -package ch.eitchnet.utils.helper; - -/** - *

- * This class implements the decoding part of RFC 4648 https://tools.ietf.org/html/rfc4648. For the encoding see - * {@link BaseEncoding} - *

- * - *

- * All versions are implemented: Base64 with URL and file name safe encoding, Base32 with HEX and Base16 - *

- * - * @author Robert von Burg - */ -public class BaseDecoding { - - // private static final Logger logger = LoggerFactory.getLogger(BaseDecoding.class); - - private static final byte PAD = '='; - - // these reverse base encoding alphabets were generated from the actual alphabet - - private 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 }; - private 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 }; - private 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 }; - private 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 }; - private 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 }; - private 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 }; - private 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[] fromBase64(byte[] bytes) { - return fromBase64(REV_BASE_64, bytes); - } - - public static byte[] fromBase64Safe(byte[] bytes) { - return fromBase64(REV_BASE_64_SAFE, bytes); - } - - public static byte[] fromBase32(byte[] bytes) { - return fromBase32(REV_BASE_32, bytes); - } - - public static byte[] fromBase32Hex(byte[] bytes) { - return fromBase32(REV_BASE_32_HEX, bytes); - } - - public static byte[] fromBase32Dmedia(byte[] bytes) { - return fromBase32(REV_BASE_32_DMEDIA, bytes); - } - - public static byte[] fromBase32Crockford(byte[] bytes) { - return fromBase32(REV_BASE_32_CROCKFORD, bytes); - } - - public static byte[] fromBase16(byte[] bytes) { - return fromBase16(REV_BASE_16, bytes); - } - - /** - * 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) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " - + (inputLength % 4)); - } - - if (alphabet.length != 128) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); - - // 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) { - - // XXX check each byte value so that it is legal - - 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; - } - -// -// /** -// * @param a -// * @return -// */ -// private static int indexOf(byte a) { -// for (int i = 0; i < BaseEncoding.BASE_64.length; i++) { -// if (BaseEncoding.BASE_64[i] == a) -// return i; -// } -// return -1; -// } -// -// private static int indexOf1(byte a) { -// for (int i = 0; i < REV_BASE_64.length; i++) { -// if (REV_BASE_64[i] == a) -// return i; -// } -// return -1; -// } - - /** - * 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) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 8, but is multiple of " - + (inputLength % 8)); - } - - if (alphabet.length != 128) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); - - // 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) { - - // XXX check each byte value so that it is legal - - 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) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " - + (bytes.length % 4)); - } - - if (alphabet.length != 128) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); - - 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++]; - - if (b1 < 0) { - throw new IllegalArgumentException("Value at index " + (i - 2) + " is not in range of alphabet (0-127)" - + b1); - } - if (b2 < 0) { - throw new IllegalArgumentException("Value at index " + (i - 1) + " is not in range of alphabet (0-127)" - + b2); - } - - byte c1 = alphabet[b1]; - byte c2 = alphabet[b2]; - - if (c1 == -1) { - throw new IllegalArgumentException("Value at index " + (i - 2) - + " is referencing illegal value in alphabet: " + b1); - } - if (c2 == -1) { - throw new IllegalArgumentException("Value at index " + (i - 2) - + " is referencing illegal value in alphabet: " + b2); - } - - int dataIndex = (i / 2) - 1; - int value = ((c1 << 4) & 0xff) | c2; - data[dataIndex] = (byte) value; - } - - return data; - } -} diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 36e070aa6..49d5f0e9e 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -23,8 +23,7 @@ package ch.eitchnet.utils.helper; /** *

- * This class implements the encoding part of RFC 4648 https://tools.ietf.org/html/rfc4648. For the decoding see - * {@link BaseDecoding}. + * This class implements the encoding and decoding of RFC 4648 https://tools.ietf.org/html/rfc4648. *

* *

@@ -38,13 +37,25 @@ package ch.eitchnet.utils.helper; * *

* + *

+ * 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 byte PAD = '='; + 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' }; @@ -70,34 +81,280 @@ public class BaseEncoding { '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 * @@ -338,7 +595,7 @@ public class BaseEncoding { * * @return the encoded data */ - public static byte[] toBase16( byte[] alphabet, byte[] bytes) { + public static byte[] toBase16(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) return new byte[0]; if (alphabet.length != 16) @@ -378,4 +635,360 @@ public class BaseEncoding { 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " + + (inputLength % 4)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + if (!isEncodedByAlphabet(alphabet, bytes, PADDING_64)) + throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); + + // 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 8, but is multiple of " + + (inputLength % 8)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + if (!isEncodedByAlphabet(alphabet, bytes, PADDING_32)) + throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); + + // 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) { + throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " + + (bytes.length % 4)); + } + + if (alphabet.length != 128) + throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + + if (!isEncodedByAlphabet(alphabet, bytes, 0)) + throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); + + 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++]; + + if (b1 < 0) { + throw new IllegalArgumentException("Value at index " + (i - 2) + " is not in range of alphabet (0-127)" + + b1); + } + if (b2 < 0) { + throw new IllegalArgumentException("Value at index " + (i - 1) + " is not in range of alphabet (0-127)" + + b2); + } + + byte c1 = alphabet[b1]; + byte c2 = alphabet[b2]; + + if (c1 == -1) { + throw new IllegalArgumentException("Value at index " + (i - 2) + + " is referencing illegal value in alphabet: " + b1); + } + if (c2 == -1) { + throw new IllegalArgumentException("Value at index " + (i - 2) + + " is referencing illegal value in alphabet: " + b2); + } + + 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/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java index 0c8cf9a19..78a9ea566 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -21,11 +21,11 @@ */ package ch.eitchnet.utils.helper; -import static ch.eitchnet.utils.helper.BaseDecoding.fromBase16; -import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32; -import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32Dmedia; -import static ch.eitchnet.utils.helper.BaseDecoding.fromBase32Hex; -import static ch.eitchnet.utils.helper.BaseDecoding.fromBase64; +import static ch.eitchnet.utils.helper.BaseEncoding.fromBase16; +import static ch.eitchnet.utils.helper.BaseEncoding.fromBase32; +import static ch.eitchnet.utils.helper.BaseEncoding.fromBase32Dmedia; +import static ch.eitchnet.utils.helper.BaseEncoding.fromBase32Hex; +import static ch.eitchnet.utils.helper.BaseEncoding.fromBase64; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import junit.framework.Assert; @@ -42,13 +42,13 @@ public class BaseDecodingTest { @Test public void testBase64() { - Assert.assertEquals("", new String(fromBase64("".getBytes()))); - Assert.assertEquals("f", new String(fromBase64("Zg==".getBytes()))); - Assert.assertEquals("fo", new String(fromBase64("Zm8=".getBytes()))); - Assert.assertEquals("foo", new String(fromBase64("Zm9v".getBytes()))); - Assert.assertEquals("foob", new String(fromBase64("Zm9vYg==".getBytes()))); - Assert.assertEquals("fooba", new String(fromBase64("Zm9vYmE=".getBytes()))); - Assert.assertEquals("foobar", new String(fromBase64("Zm9vYmFy".getBytes()))); + Assert.assertEquals("", fromBase64("")); + Assert.assertEquals("f", fromBase64("Zg==")); + Assert.assertEquals("fo", fromBase64("Zm8=")); + Assert.assertEquals("foo", fromBase64("Zm9v")); + Assert.assertEquals("foob", fromBase64("Zm9vYg==")); + Assert.assertEquals("fooba", fromBase64("Zm9vYmE=")); + Assert.assertEquals("foobar", fromBase64("Zm9vYmFy")); byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -64,13 +64,13 @@ public class BaseDecodingTest { @Test public void testBase32() { - Assert.assertEquals("", new String(fromBase32("".getBytes()))); - Assert.assertEquals("f", new String(fromBase32("MY======".getBytes()))); - Assert.assertEquals("fo", new String(fromBase32("MZXQ====".getBytes()))); - Assert.assertEquals("foo", new String(fromBase32("MZXW6===".getBytes()))); - Assert.assertEquals("foob", new String(fromBase32("MZXW6YQ=".getBytes()))); - Assert.assertEquals("fooba", new String(fromBase32("MZXW6YTB".getBytes()))); - Assert.assertEquals("foobar", new String(fromBase32("MZXW6YTBOI======".getBytes()))); + Assert.assertEquals("", fromBase32("")); + Assert.assertEquals("f", fromBase32("MY======")); + Assert.assertEquals("fo", fromBase32("MZXQ====")); + Assert.assertEquals("foo", fromBase32("MZXW6===")); + Assert.assertEquals("foob", fromBase32("MZXW6YQ=")); + Assert.assertEquals("fooba", fromBase32("MZXW6YTB")); + Assert.assertEquals("foobar", fromBase32("MZXW6YTBOI======")); byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -86,13 +86,13 @@ public class BaseDecodingTest { @Test public void testBase32Hex() { - Assert.assertEquals("", new String(fromBase32Hex("".getBytes()))); - Assert.assertEquals("f", new String(fromBase32Hex("CO======".getBytes()))); - Assert.assertEquals("fo", new String(fromBase32Hex("CPNG====".getBytes()))); - Assert.assertEquals("foo", new String(fromBase32Hex("CPNMU===".getBytes()))); - Assert.assertEquals("foob", new String(fromBase32Hex("CPNMUOG=".getBytes()))); - Assert.assertEquals("fooba", new String(fromBase32Hex("CPNMUOJ1".getBytes()))); - Assert.assertEquals("foobar", new String(fromBase32Hex("CPNMUOJ1E8======".getBytes()))); + Assert.assertEquals("", fromBase32Hex("")); + Assert.assertEquals("f", fromBase32Hex("CO======")); + Assert.assertEquals("fo", fromBase32Hex("CPNG====")); + Assert.assertEquals("foo", fromBase32Hex("CPNMU===")); + Assert.assertEquals("foob", fromBase32Hex("CPNMUOG=")); + Assert.assertEquals("fooba", fromBase32Hex("CPNMUOJ1")); + Assert.assertEquals("foobar", fromBase32Hex("CPNMUOJ1E8======")); byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -109,14 +109,14 @@ public class BaseDecodingTest { @Test public void testBase32Dmedia() { - Assert.assertEquals("", new String(fromBase32Dmedia("".getBytes()))); - Assert.assertEquals("binary foo", new String(fromBase32Dmedia("FCNPVRELI7J9FUUI".getBytes()))); - Assert.assertEquals("f", new String(fromBase32Dmedia("FR======".getBytes()))); - Assert.assertEquals("fo", new String(fromBase32Dmedia("FSQJ====".getBytes()))); - Assert.assertEquals("foo", new String(fromBase32Dmedia("FSQPX===".getBytes()))); - Assert.assertEquals("foob", new String(fromBase32Dmedia("FSQPXRJ=".getBytes()))); - Assert.assertEquals("fooba", new String(fromBase32Dmedia("FSQPXRM4".getBytes()))); - Assert.assertEquals("foobar", new String(fromBase32Dmedia("FSQPXRM4HB======".getBytes()))); + Assert.assertEquals("", fromBase32Dmedia("")); + Assert.assertEquals("binary foo", fromBase32Dmedia("FCNPVRELI7J9FUUI")); + Assert.assertEquals("f", fromBase32Dmedia("FR======")); + Assert.assertEquals("fo", fromBase32Dmedia("FSQJ====")); + Assert.assertEquals("foo", fromBase32Dmedia("FSQPX===")); + Assert.assertEquals("foob", fromBase32Dmedia("FSQPXRJ=")); + Assert.assertEquals("fooba", fromBase32Dmedia("FSQPXRM4")); + Assert.assertEquals("foobar", fromBase32Dmedia("FSQPXRM4HB======")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -129,13 +129,13 @@ public class BaseDecodingTest { @Test public void testBase16() { - Assert.assertEquals("", new String(fromBase16("".getBytes()))); - Assert.assertEquals("f", new String(fromBase16("66".getBytes()))); - Assert.assertEquals("fo", new String(fromBase16("666F".getBytes()))); - Assert.assertEquals("foo", new String(fromBase16("666F6F".getBytes()))); - Assert.assertEquals("foob", new String(fromBase16("666F6F62".getBytes()))); - Assert.assertEquals("fooba", new String(fromBase16("666F6F6261".getBytes()))); - Assert.assertEquals("foobar", new String(fromBase16("666F6F626172".getBytes()))); + Assert.assertEquals("", fromBase16("")); + Assert.assertEquals("f", fromBase16("66")); + Assert.assertEquals("fo", fromBase16("666F")); + Assert.assertEquals("foo", fromBase16("666F6F")); + Assert.assertEquals("foob", fromBase16("666F6F62")); + Assert.assertEquals("fooba", fromBase16("666F6F6261")); + Assert.assertEquals("foobar", fromBase16("666F6F626172")); byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index 0a7042dae..7a2b3052f 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -42,13 +42,13 @@ public class BaseEncodingTest { @Test public void testBase64() { - Assert.assertEquals("", new String(toBase64("".getBytes()))); - Assert.assertEquals("Zg==", new String(toBase64("f".getBytes()))); - Assert.assertEquals("Zm8=", new String(toBase64("fo".getBytes()))); - Assert.assertEquals("Zm9v", new String(toBase64("foo".getBytes()))); - Assert.assertEquals("Zm9vYg==", new String(toBase64("foob".getBytes()))); - Assert.assertEquals("Zm9vYmE=", new String(toBase64("fooba".getBytes()))); - Assert.assertEquals("Zm9vYmFy", new String(toBase64("foobar".getBytes()))); + Assert.assertEquals("", toBase64("")); + Assert.assertEquals("Zg==", toBase64("f")); + Assert.assertEquals("Zm8=", toBase64("fo")); + Assert.assertEquals("Zm9v", toBase64("foo")); + Assert.assertEquals("Zm9vYg==", toBase64("foob")); + Assert.assertEquals("Zm9vYmE=", toBase64("fooba")); + Assert.assertEquals("Zm9vYmFy", toBase64("foobar")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -61,13 +61,13 @@ public class BaseEncodingTest { @Test public void testBase32() { - Assert.assertEquals("", new String(toBase32("".getBytes()))); - Assert.assertEquals("MY======", new String(toBase32("f".getBytes()))); - Assert.assertEquals("MZXQ====", new String(toBase32("fo".getBytes()))); - Assert.assertEquals("MZXW6===", new String(toBase32("foo".getBytes()))); - Assert.assertEquals("MZXW6YQ=", new String(toBase32("foob".getBytes()))); - Assert.assertEquals("MZXW6YTB", new String(toBase32("fooba".getBytes()))); - Assert.assertEquals("MZXW6YTBOI======", new String(toBase32("foobar".getBytes()))); + Assert.assertEquals("", toBase32("")); + Assert.assertEquals("MY======", toBase32("f")); + Assert.assertEquals("MZXQ====", toBase32("fo")); + Assert.assertEquals("MZXW6===", toBase32("foo")); + Assert.assertEquals("MZXW6YQ=", toBase32("foob")); + Assert.assertEquals("MZXW6YTB", toBase32("fooba")); + Assert.assertEquals("MZXW6YTBOI======", toBase32("foobar")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -80,13 +80,13 @@ public class BaseEncodingTest { @Test public void testBase32Hex() { - Assert.assertEquals("", new String(toBase32Hex("".getBytes()))); - Assert.assertEquals("CO======", new String(toBase32Hex("f".getBytes()))); - Assert.assertEquals("CPNG====", new String(toBase32Hex("fo".getBytes()))); - Assert.assertEquals("CPNMU===", new String(toBase32Hex("foo".getBytes()))); - Assert.assertEquals("CPNMUOG=", new String(toBase32Hex("foob".getBytes()))); - Assert.assertEquals("CPNMUOJ1", new String(toBase32Hex("fooba".getBytes()))); - Assert.assertEquals("CPNMUOJ1E8======", new String(toBase32Hex("foobar".getBytes()))); + Assert.assertEquals("", toBase32Hex("")); + Assert.assertEquals("CO======", toBase32Hex("f")); + Assert.assertEquals("CPNG====", toBase32Hex("fo")); + Assert.assertEquals("CPNMU===", toBase32Hex("foo")); + Assert.assertEquals("CPNMUOG=", toBase32Hex("foob")); + Assert.assertEquals("CPNMUOJ1", toBase32Hex("fooba")); + Assert.assertEquals("CPNMUOJ1E8======", toBase32Hex("foobar")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -96,18 +96,18 @@ public class BaseEncodingTest { long end = System.nanoTime(); logger.info("Encoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); } - + @Test public void testBase32Dmedia() { - - Assert.assertEquals("", new String(toBase32Dmedia("".getBytes()))); - Assert.assertEquals("FCNPVRELI7J9FUUI", new String(toBase32Dmedia("binary foo".getBytes()))); - Assert.assertEquals("FR======", new String(toBase32Dmedia("f".getBytes()))); - Assert.assertEquals("FSQJ====", new String(toBase32Dmedia("fo".getBytes()))); - Assert.assertEquals("FSQPX===", new String(toBase32Dmedia("foo".getBytes()))); - Assert.assertEquals("FSQPXRJ=", new String(toBase32Dmedia("foob".getBytes()))); - Assert.assertEquals("FSQPXRM4", new String(toBase32Dmedia("fooba".getBytes()))); - Assert.assertEquals("FSQPXRM4HB======", new String(toBase32Dmedia("foobar".getBytes()))); + + Assert.assertEquals("", toBase32Dmedia("")); + Assert.assertEquals("FCNPVRELI7J9FUUI", toBase32Dmedia("binary foo")); + Assert.assertEquals("FR======", toBase32Dmedia("f")); + Assert.assertEquals("FSQJ====", toBase32Dmedia("fo")); + Assert.assertEquals("FSQPX===", toBase32Dmedia("foo")); + Assert.assertEquals("FSQPXRJ=", toBase32Dmedia("foob")); + Assert.assertEquals("FSQPXRM4", toBase32Dmedia("fooba")); + Assert.assertEquals("FSQPXRM4HB======", toBase32Dmedia("foobar")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -120,13 +120,13 @@ public class BaseEncodingTest { @Test public void testBase16() { - Assert.assertEquals("", new String(toBase16("".getBytes()))); - Assert.assertEquals("66", new String(toBase16("f".getBytes()))); - Assert.assertEquals("666F", new String(toBase16("fo".getBytes()))); - Assert.assertEquals("666F6F", new String(toBase16("foo".getBytes()))); - Assert.assertEquals("666F6F62", new String(toBase16("foob".getBytes()))); - Assert.assertEquals("666F6F6261", new String(toBase16("fooba".getBytes()))); - Assert.assertEquals("666F6F626172", new String(toBase16("foobar".getBytes()))); + Assert.assertEquals("", toBase16("")); + Assert.assertEquals("66", toBase16("f")); + Assert.assertEquals("666F", toBase16("fo")); + Assert.assertEquals("666F6F", toBase16("foo")); + Assert.assertEquals("666F6F62", toBase16("foob")); + Assert.assertEquals("666F6F6261", toBase16("fooba")); + Assert.assertEquals("666F6F626172", toBase16("foobar")); long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; From e5872299145daa3cfd752872e1540b38667db658 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 10 Mar 2013 23:58:06 +0100 Subject: [PATCH 025/180] [New] added new StringHelper.hash*AsHex() methods as a nice API --- .../eitchnet/utils/helper/StringHelper.java | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index bbd6def33..e345c7157 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -39,8 +39,8 @@ public class StringHelper { /** * Hex char table for fast calculating of hex value */ - private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', - 'f' }; + private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', + 'd', 'e', 'f' }; /** * Converts each byte of the given byte array to a HEX value and returns the concatenation of these values @@ -94,7 +94,19 @@ public class StringHelper { } /** - * Generates the MD5 Hash of a string Use {@link StringHelper#getHexString(byte[])} to convert the byte array to a + * 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(StringHelper.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 @@ -119,6 +131,18 @@ public class StringHelper { return StringHelper.hash("MD5", bytes); } + /** + * 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(StringHelper.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 @@ -145,6 +169,18 @@ public class StringHelper { return StringHelper.hash("SHA-1", bytes); } + /** + * 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(StringHelper.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 From a94981e7419383893ed9582641ea39fabc537623 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 11 Mar 2013 21:23:50 +0100 Subject: [PATCH 026/180] [New] Added new methods to FileHelper to read and write byte arrays. Fixed missing close on existing methods --- .../ch/eitchnet/utils/helper/FileHelper.java | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 0bcbbf1f6..a175dc4c5 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -46,13 +46,57 @@ import org.slf4j.LoggerFactory; */ 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", + humanizeFileSize(MAX_FILE_SIZE), humanizeFileSize(file.length()))); + + byte[] data = new byte[(int) file.length()]; + int pos = 0; + + BufferedInputStream in = null; + try { + 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()); + } catch (IOException e) { + throw new RuntimeException("Could not read file " + file.getAbsolutePath()); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + FileHelper.logger.error("Failed to close InputStream: " + e.getLocalizedMessage()); + } + } + } + + 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) { @@ -86,16 +130,51 @@ public class FileHelper { } } + /** + * 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) { + + BufferedOutputStream out = null; + try { + + out = new BufferedOutputStream(new FileOutputStream(dstFile)); + out.write(bytes); + + } catch (FileNotFoundException e) { + throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + FileHelper.logger.error("Failed to close OutputStream: " + e.getLocalizedMessage()); + } + } + } + } + /** * 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) { + + BufferedWriter bufferedwriter = null; try { - BufferedWriter bufferedwriter = new BufferedWriter(new FileWriter(dstFile)); + bufferedwriter = new BufferedWriter(new FileWriter(dstFile)); bufferedwriter.write(string); bufferedwriter.close(); @@ -104,6 +183,14 @@ public class FileHelper { throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); } catch (IOException e) { throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); + } finally { + if (bufferedwriter != null) { + try { + bufferedwriter.close(); + } catch (IOException e) { + FileHelper.logger.error("Failed to close BufferedWriter: " + e.getLocalizedMessage()); + } + } } } From cca9426358f5f25905721e349139df50d08e921e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 11 Mar 2013 22:50:05 +0100 Subject: [PATCH 027/180] [Minor] changed alphabet of StringHelper.getHexString() to have capital letters, this corresponds to RFC4648 --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index e345c7157..c6cb8841b 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -39,8 +39,8 @@ public class StringHelper { /** * Hex char table for fast calculating of hex value */ - private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', - 'd', 'e', 'f' }; + private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', + 'D', 'E', 'F' }; /** * Converts each byte of the given byte array to a HEX value and returns the concatenation of these values From 841fedafdfbf97633bb467a70241e1d04aade301 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 13 Mar 2013 17:13:27 +0100 Subject: [PATCH 028/180] Update README.md Added information which classes exist and what they are for. Added how to build description --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index acb872905..1ed357451 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ ch.eitchnet.java.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 6 +* JUnit 4.10 (only during tests) +* slf4j 1.7.2 +* slf4j-log4j bindings (only during tests) + +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 From 7700694a84284f6cdd301d69d0e8dbca3a31753c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 9 Apr 2013 19:25:07 +0200 Subject: [PATCH 029/180] [New] add new method ArraysHelper.copyOf(byte[]) --- .../eitchnet/utils/helper/ArraysHelper.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java index c54ac3e58..74a22a8cf 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java @@ -21,18 +21,41 @@ */ package ch.eitchnet.utils.helper; +import java.util.Arrays; + /** * @author Robert von Burg * */ public class ArraysHelper { + /** + * Returns true if the byte array contains the given byte value + * + * @param bytes + * the array to search in + * @param searchByte + * the value to search for + * + * @return true if found, false if not + */ public static boolean contains(byte[] bytes, byte searchByte) { for (byte b : bytes) { if (b == searchByte) return true; } - return false; } + + /** + * Creates a simple copy of the given array + * + * @param bytes + * the array to copy + * + * @return the copy + */ + public static byte[] copyOf(byte[] bytes) { + return Arrays.copyOf(bytes, bytes.length); + } } From d2ee280dbe3dda20edfbbdfc20df7ca011e61348 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 8 Aug 2013 00:00:18 +0200 Subject: [PATCH 030/180] [New] Refactored the ObjectCache to not used a generic T in the class There was no use and it only made the code unreadable. --- .../utils/objectfilter/ObjectCache.java | 12 +- .../utils/objectfilter/ObjectFilter.java | 135 ++++++++---------- 2 files changed, 67 insertions(+), 80 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index 57e6a0132..dfa3ea8ac 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -36,10 +36,8 @@ import org.slf4j.LoggerFactory; *

* * @author Michael Gatto - * - * @param */ -public class ObjectCache { +public class ObjectCache { private final static Logger logger = LoggerFactory.getLogger(ObjectCache.class); @@ -54,7 +52,7 @@ public class ObjectCache { /** * object The object that shall be cached */ - private T object; + private ITransactionObject object; /** * operation The operation that has occurred on this object. */ @@ -65,7 +63,7 @@ public class ObjectCache { * @param object * @param operation */ - public ObjectCache(String key, T object, Operation operation) { + public ObjectCache(String key, ITransactionObject object, Operation operation) { this.id = object.getTransactionID(); this.key = key; @@ -83,7 +81,7 @@ public class ObjectCache { * * @param object */ - public void setObject(T object) { + public void setObject(ITransactionObject object) { if (ObjectCache.logger.isDebugEnabled()) { ObjectCache.logger.debug("Updating ID " + this.id + " to value " + object.toString()); } @@ -120,7 +118,7 @@ public class ObjectCache { /** * @return the object */ - public T getObject() { + public ITransactionObject getObject() { return this.object; } diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index f7b93124b..dc1f7194f 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -19,6 +19,7 @@ */ package ch.eitchnet.utils.objectfilter; +import java.text.MessageFormat; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -76,16 +77,12 @@ import org.slf4j.LoggerFactory; * * @author Michael Gatto (initial version) * @author Robert von Burg - * - * @param */ -public class ObjectFilter { - - // XXX think about removing the generic T, as there is no sense in it +public class ObjectFilter { private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class); - private HashMap> cache = new HashMap>(); + private HashMap cache = new HashMap(); private HashSet keySet = new HashSet(); private static long id = ITransactionObject.UNSET; @@ -116,7 +113,7 @@ public class ObjectFilter { * @param objectToAdd * The object for which addition shall be registered. */ - public void add(String key, T objectToAdd) { + public void add(String key, ITransactionObject objectToAdd) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("add object " + objectToAdd + " with key " + key); @@ -131,29 +128,21 @@ public class ObjectFilter { // run. Hence, we create an ID and add it to the cache. id = dispenseID(); objectToAdd.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); + ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); this.cache.put(id, cacheObj); } else { - ObjectCache cached = this.cache.get(Long.valueOf(objectToAdd.getTransactionID())); + ObjectCache cached = this.cache.get(Long.valueOf(objectToAdd.getTransactionID())); if (cached == null) { // The object got an ID during this run, but was not added to the cache. // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); + ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { - throw new RuntimeException( - "Invalid key provided for object with transaction ID " - + Long.toString(id) - + " and operation " - + Operation.ADD.toString() - + ": existing key is " - + existingKey - + ", new key is " - + key - + ". Object may be present in the same filter instance only once, registered using one key only. Object:" - + objectToAdd.toString()); + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + throw new RuntimeException(MessageFormat.format(msg, Long.toString(id), Operation.ADD.toString(), + existingKey, key, objectToAdd.toString())); } // The object is in cache: update the version as required, keeping in mind that most // of the cases here will be mistakes... @@ -196,7 +185,7 @@ public class ObjectFilter { * @param objectToUpdate * The object for which update shall be registered. */ - public void update(String key, T objectToUpdate) { + public void update(String key, ITransactionObject objectToUpdate) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("update object " + objectToUpdate + " with key " + key); @@ -209,14 +198,14 @@ public class ObjectFilter { if (id == ITransactionObject.UNSET) { id = dispenseID(); objectToUpdate.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); + ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); this.cache.put(id, cacheObj); } else { - ObjectCache cached = this.cache.get(Long.valueOf(objectToUpdate.getTransactionID())); + ObjectCache cached = this.cache.get(Long.valueOf(objectToUpdate.getTransactionID())); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); + ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); @@ -274,7 +263,7 @@ public class ObjectFilter { * @param objectToRemove * The object for which removal shall be registered. */ - public void remove(String key, T objectToRemove) { + public void remove(String key, ITransactionObject objectToRemove) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("remove object " + objectToRemove + " with key " + key); @@ -286,14 +275,14 @@ public class ObjectFilter { if (id == ITransactionObject.UNSET) { id = dispenseID(); objectToRemove.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); + ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); this.cache.put(id, cacheObj); } else { - ObjectCache cached = this.cache.get(Long.valueOf(id)); + ObjectCache cached = this.cache.get(Long.valueOf(id)); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); + ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); this.cache.put(id, cacheObj); } else { String existingKey = cached.getKey(); @@ -337,8 +326,8 @@ public class ObjectFilter { * @param addedObjects * The objects for which addition shall be registered. */ - public void addAll(String key, Collection addedObjects) { - for (T addObj : addedObjects) { + public void addAll(String key, Collection addedObjects) { + for (ITransactionObject addObj : addedObjects) { add(key, addObj); } } @@ -351,8 +340,8 @@ public class ObjectFilter { * @param updatedObjects * The objects for which update shall be registered. */ - public void updateAll(String key, Collection updatedObjects) { - for (T update : updatedObjects) { + public void updateAll(String key, Collection updatedObjects) { + for (ITransactionObject update : updatedObjects) { update(key, update); } } @@ -365,8 +354,8 @@ public class ObjectFilter { * @param removedObjects * The objects for which removal shall be registered. */ - public void removeAll(String key, Collection removedObjects) { - for (T removed : removedObjects) { + public void removeAll(String key, Collection removedObjects) { + for (ITransactionObject removed : removedObjects) { remove(key, removed); } } @@ -377,7 +366,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for addition */ - public void add(T object) { + public void add(ITransactionObject object) { add(object.getClass().getName(), object); } @@ -387,7 +376,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for updating */ - public void update(T object) { + public void update(ITransactionObject object) { update(object.getClass().getName(), object); } @@ -397,7 +386,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for removal */ - public void remove(T object) { + public void remove(ITransactionObject object) { remove(object.getClass().getName(), object); } @@ -408,8 +397,8 @@ public class ObjectFilter { * @param objects * The objects that shall be registered for addition */ - public void addAll(List objects) { - for (T addedObj : objects) { + public void addAll(List objects) { + for (ITransactionObject addedObj : objects) { add(addedObj.getClass().getName(), addedObj); } } @@ -421,8 +410,8 @@ public class ObjectFilter { * @param updateObjects * The objects that shall be registered for updating */ - public void updateAll(List updateObjects) { - for (T update : updateObjects) { + public void updateAll(List updateObjects) { + for (ITransactionObject update : updateObjects) { update(update.getClass().getName(), update); } } @@ -434,8 +423,8 @@ public class ObjectFilter { * @param removedObjects * The objects that shall be registered for removal */ - public void removeAll(List removedObjects) { - for (T removed : removedObjects) { + public void removeAll(List removedObjects) { + for (ITransactionObject removed : removedObjects) { remove(removed.getClass().getName(), removed); } } @@ -447,10 +436,10 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of all objects registered under the given key and that need to be added. */ - public List getAdded(String key) { - List addedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + public List getAdded(String key) { + List addedObjects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { addedObjects.add(objectCache.getObject()); } @@ -467,10 +456,10 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of all objects registered under the given key and that need to be added. */ - public List getAdded(Class clazz, String key) { + public List getAdded(Class clazz, String key) { List addedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") @@ -489,10 +478,10 @@ public class ObjectFilter { * registration key of the objects to match * @return The list of all objects registered under the given key and that need to be updated. */ - public List getUpdated(String key) { - List updatedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + public List getUpdated(String key) { + List updatedObjects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { updatedObjects.add(objectCache.getObject()); } @@ -507,10 +496,10 @@ public class ObjectFilter { * registration key of the objects to match * @return The list of all objects registered under the given key and that need to be updated. */ - public List getUpdated(Class clazz, String key) { + public List getUpdated(Class clazz, String key) { List updatedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") @@ -529,10 +518,10 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of object registered under the given key that have, as a final action, removal. */ - public List getRemoved(String key) { - List removedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + public List getRemoved(String key) { + List removedObjects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { removedObjects.add(objectCache.getObject()); } @@ -547,10 +536,10 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of object registered under the given key that have, as a final action, removal. */ - public List getRemoved(Class clazz, String key) { + public List getRemoved(Class clazz, String key) { List removedObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") @@ -570,10 +559,10 @@ public class ObjectFilter { * The registration key for which the objects shall be retrieved * @return The list of objects matching the given key. */ - public List getAll(String key) { - List allObjects = new LinkedList(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + public List getAll(String key) { + List allObjects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allObjects.add(objectCache.getObject()); } @@ -589,10 +578,10 @@ public class ObjectFilter { * The registration key for which the objects shall be retrieved * @return The list of objects matching the given key. */ - public List> getCache(String key) { - List> allCache = new LinkedList>(); - Collection> allObjs = this.cache.values(); - for (ObjectCache objectCache : allObjs) { + public List getCache(String key) { + List allCache = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allCache.add(objectCache); } From 2bc24c68c8942d517fe5b4aed62b30cdd0fb403c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 8 Aug 2013 00:00:47 +0200 Subject: [PATCH 031/180] [New] set new version to 0.2.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ddd624b1d..ed75a6af3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ ch.eitchnet ch.eitchnet.utils jar - 0.1.0-SNAPSHOT + 0.2.0-SNAPSHOT ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse https://github.com/eitch/ch.eitchnet.utils From 4263f4693488057d0837b3ccad4b34c720e38159 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 10 Aug 2013 00:37:21 +0200 Subject: [PATCH 032/180] [Major] Refactoring the ObjectFilter to no longer rely on an interface Now the objects put into the cache no longer need to implement an interface, this makes it easier to adopt the filter in different scenarios. Tests written to validate that the filter works as expected. Still some outstanding tests which currently fail --- .../objectfilter/ITransactionObject.java | 56 ---- .../utils/objectfilter/ObjectCache.java | 48 +-- .../utils/objectfilter/ObjectFilter.java | 278 ++++++++---------- .../utils/objectfilter/ObjectFilterTest.java | 247 ++++++++++++++++ 4 files changed, 403 insertions(+), 226 deletions(-) delete mode 100644 src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java create mode 100644 src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java b/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java deleted file mode 100644 index c9bfc3efe..000000000 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ITransactionObject.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2012 - * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . - * - */ -package ch.eitchnet.utils.objectfilter; - -/** - * This interface serves for objects which are required, at some point, to have a unique ID within a transaction. - * - * @author Michael Gatto - */ -public interface ITransactionObject { - - /** - * UNSET Marker to determine if ids have not been set. - *

- * Beware: this is set to 0 due to transient field in the {@link ITransactionObject} implementations that store the - * ID, which are set to zero when de-serialized, and that are not allowed to be serialized. - *

- */ - public static final long UNSET = 0; - - /** - * Set the ID of this object. This ID is unique for this object within the transaction. - * - * @param id - * The ID to set. - */ - public void setTransactionID(long id); - - /** - * @return The ID of this object, as set within the transaction. This ID shall guarantee that it is unique within - * this transaction. - */ - public long getTransactionID(); - - /** - * Reset / anul the transaction ID of this object - */ - public void resetTransactionID(); -} diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index dfa3ea8ac..382baffc8 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -36,42 +36,52 @@ import org.slf4j.LoggerFactory; *

* * @author Michael Gatto + * @author Robert von Burg */ public class ObjectCache { private final static Logger logger = LoggerFactory.getLogger(ObjectCache.class); + /** + * UNSET Marker to determine if ids have not been set. + */ + public static final long UNSET = 0; + /** * id The unique ID of this object in this session */ private final long id; + /** * key The key defining who's registered for this object's state */ private final String key; - /** - * object The object that shall be cached - */ - private ITransactionObject object; + /** * operation The operation that has occurred on this object. */ private Operation operation; /** + * object The object that shall be cached + */ + private Object object; + + /** + * @param id * @param key * @param object * @param operation */ - public ObjectCache(String key, ITransactionObject object, Operation operation) { + public ObjectCache(long id, String key, Object object, Operation operation) { - this.id = object.getTransactionID(); + this.id = id; this.key = key; this.object = object; this.operation = operation; - if (ObjectCache.logger.isDebugEnabled()) { - ObjectCache.logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + if (logger.isDebugEnabled()) { + logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation + " / " + object.toString()); } } @@ -81,9 +91,9 @@ public class ObjectCache { * * @param object */ - public void setObject(ITransactionObject object) { - if (ObjectCache.logger.isDebugEnabled()) { - ObjectCache.logger.debug("Updating ID " + this.id + " to value " + object.toString()); + public void setObject(Object object) { + if (logger.isDebugEnabled()) { + logger.debug("Updating ID " + this.id + " to value " + object.toString()); } this.object = object; } @@ -114,18 +124,18 @@ public class ObjectCache { public String getKey() { return this.key; } - - /** - * @return the object - */ - public ITransactionObject getObject() { - return this.object; - } - + /** * @return the operation */ public Operation getOperation() { return this.operation; } + + /** + * @return the object + */ + public Object getObject() { + return this.object; + } } diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index dc1f7194f..7ee255595 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -76,16 +76,16 @@ import org.slf4j.LoggerFactory; * * * @author Michael Gatto (initial version) - * @author Robert von Burg + * @author Robert von Burg (minor modifications, refactorings) */ public class ObjectFilter { private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class); - private HashMap cache = new HashMap(); + private HashMap cache = new HashMap(); private HashSet keySet = new HashSet(); - private static long id = ITransactionObject.UNSET; + private static long id = ObjectCache.UNSET; /** * Register, under the given key, the addition of the given object. @@ -113,7 +113,7 @@ public class ObjectFilter { * @param objectToAdd * The object for which addition shall be registered. */ - public void add(String key, ITransactionObject objectToAdd) { + public void add(String key, Object objectToAdd) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("add object " + objectToAdd + " with key " + key); @@ -122,43 +122,37 @@ public class ObjectFilter { this.keySet.add(key); // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. - long id = objectToAdd.getTransactionID(); - if (id == ITransactionObject.UNSET) { - // The ID of the object has not been set, so it has not been in the cache during this - // run. Hence, we create an ID and add it to the cache. - id = dispenseID(); - objectToAdd.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); - this.cache.put(id, cacheObj); + ObjectCache cached = this.cache.get(objectToAdd); + if (cached == null) { + + // The object has not yet been added to the cache. + // Hence, we add it now, with the ADD operation. + ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToAdd, Operation.ADD); + this.cache.put(objectToAdd, cacheObj); + } else { - ObjectCache cached = this.cache.get(Long.valueOf(objectToAdd.getTransactionID())); - if (cached == null) { - // The object got an ID during this run, but was not added to the cache. - // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToAdd, Operation.ADD); - this.cache.put(id, cacheObj); - } else { - String existingKey = cached.getKey(); - if (!existingKey.equals(key)) { - String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; - throw new RuntimeException(MessageFormat.format(msg, Long.toString(id), Operation.ADD.toString(), - existingKey, key, objectToAdd.toString())); - } - // The object is in cache: update the version as required, keeping in mind that most - // of the cases here will be mistakes... - Operation op = cached.getOperation(); - switch (op) { - case ADD: - throw new RuntimeException("Stale State exception. Invalid + after +"); - case MODIFY: - throw new RuntimeException("Stale State exception. Invalid + after +="); - case REMOVE: - cached.setObject(objectToAdd); - cached.setOperation(Operation.MODIFY); - break; - } // switch - }// else of object not in cache - }// else of ID not set + + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), + Operation.ADD.toString(), existingKey, key, objectToAdd.toString())); + } + + // The object is in cache: update the version as required, keeping in mind that most + // of the cases here will be mistakes... + Operation op = cached.getOperation(); + switch (op) { + case ADD: + throw new IllegalStateException("Stale State exception: Invalid + after +"); + case MODIFY: + throw new IllegalStateException("Stale State exception: Invalid + after +="); + case REMOVE: + cached.setObject(objectToAdd); + cached.setOperation(Operation.MODIFY); + break; + } // switch + }// else of object not in cache } /** @@ -185,58 +179,45 @@ public class ObjectFilter { * @param objectToUpdate * The object for which update shall be registered. */ - public void update(String key, ITransactionObject objectToUpdate) { + public void update(String key, Object objectToUpdate) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("update object " + objectToUpdate + " with key " + key); // add the key to the keyset this.keySet.add(key); + // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. + ObjectCache cached = this.cache.get(objectToUpdate); + if (cached == null) { + + // The object got an ID during this run, but was not added to this cache. + // Hence, we add it now, with the current operation. + ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToUpdate, Operation.MODIFY); + this.cache.put(objectToUpdate, cacheObj); - long id = objectToUpdate.getTransactionID(); - if (id == ITransactionObject.UNSET) { - id = dispenseID(); - objectToUpdate.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); - this.cache.put(id, cacheObj); } else { - ObjectCache cached = this.cache.get(Long.valueOf(objectToUpdate.getTransactionID())); - if (cached == null) { - // The object got an ID during this run, but was not added to this cache. - // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToUpdate, Operation.MODIFY); - this.cache.put(id, cacheObj); - } else { - String existingKey = cached.getKey(); - if (!existingKey.equals(key)) { - throw new RuntimeException( - "Invalid key provided for object with transaction ID " - + Long.toString(id) - + " and operation " - + Operation.MODIFY.toString() - + ": existing key is " - + existingKey - + ", new key is " - + key - + ". Object may be present in the same filter instance only once, registered using one key only. Object:" - + objectToUpdate.toString()); - } - // The object is in cache: update the version as required. - Operation op = cached.getOperation(); - switch (op) { - case ADD: - cached.setObject(objectToUpdate); - break; - case MODIFY: - cached.setObject(objectToUpdate); - break; - case REMOVE: - throw new RuntimeException("Stale State exception: Invalid += after -"); - } // switch - }// else of object not in cache - }// else of ID not set + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), + Operation.MODIFY.toString(), existingKey, key, objectToUpdate.toString())); + } + + // The object is in cache: update the version as required. + Operation op = cached.getOperation(); + switch (op) { + case ADD: + cached.setObject(objectToUpdate); + break; + case MODIFY: + cached.setObject(objectToUpdate); + break; + case REMOVE: + throw new IllegalStateException("Stale State exception: Invalid += after -"); + } // switch + }// else of object not in cache } /** @@ -263,58 +244,45 @@ public class ObjectFilter { * @param objectToRemove * The object for which removal shall be registered. */ - public void remove(String key, ITransactionObject objectToRemove) { + public void remove(String key, Object objectToRemove) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("remove object " + objectToRemove + " with key " + key); // add the key to the keyset this.keySet.add(key); + // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. - long id = objectToRemove.getTransactionID(); - if (id == ITransactionObject.UNSET) { - id = dispenseID(); - objectToRemove.setTransactionID(id); - ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); - this.cache.put(id, cacheObj); + ObjectCache cached = this.cache.get(objectToRemove); + if (cached == null) { + // The object got an ID during this run, but was not added to this cache. + // Hence, we add it now, with the current operation. + ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToRemove, Operation.REMOVE); + this.cache.put(objectToRemove, cacheObj); } else { - ObjectCache cached = this.cache.get(Long.valueOf(id)); - if (cached == null) { - // The object got an ID during this run, but was not added to this cache. - // Hence, we add it now, with the current operation. - ObjectCache cacheObj = new ObjectCache(key, objectToRemove, Operation.REMOVE); - this.cache.put(id, cacheObj); - } else { - String existingKey = cached.getKey(); - if (!existingKey.equals(key)) { - throw new RuntimeException( - "Invalid key provided for object with transaction ID " - + Long.toString(id) - + " and operation " - + Operation.REMOVE.toString() - + ": existing key is " - + existingKey - + ", new key is " - + key - + ". Object may be present in the same filter instance only once, registered using one key only. Object:" - + objectToRemove.toString()); - } - // The object is in cache: update the version as required. - Operation op = cached.getOperation(); - switch (op) { - case ADD: - // this is a case where we're removing the object from the cache, since we are - // removing it now and it was added previously. - this.cache.remove(Long.valueOf(id)); - break; - case MODIFY: - cached.setObject(objectToRemove); - cached.setOperation(Operation.REMOVE); - break; - case REMOVE: - throw new RuntimeException("Stale State exception. Invalid - after -"); - } // switch + + String existingKey = cached.getKey(); + if (!existingKey.equals(key)) { + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), + Operation.REMOVE.toString(), existingKey, key, objectToRemove.toString())); } + + // The object is in cache: update the version as required. + Operation op = cached.getOperation(); + switch (op) { + case ADD: + // this is a case where we're removing the object from the cache, since we are + // removing it now and it was added previously. + this.cache.remove(objectToRemove); + break; + case MODIFY: + cached.setObject(objectToRemove); + cached.setOperation(Operation.REMOVE); + break; + case REMOVE: + throw new IllegalStateException("Stale State exception: Invalid - after -"); + } // switch } } @@ -326,8 +294,8 @@ public class ObjectFilter { * @param addedObjects * The objects for which addition shall be registered. */ - public void addAll(String key, Collection addedObjects) { - for (ITransactionObject addObj : addedObjects) { + public void addAll(String key, Collection addedObjects) { + for (Object addObj : addedObjects) { add(key, addObj); } } @@ -340,8 +308,8 @@ public class ObjectFilter { * @param updatedObjects * The objects for which update shall be registered. */ - public void updateAll(String key, Collection updatedObjects) { - for (ITransactionObject update : updatedObjects) { + public void updateAll(String key, Collection updatedObjects) { + for (Object update : updatedObjects) { update(key, update); } } @@ -354,8 +322,8 @@ public class ObjectFilter { * @param removedObjects * The objects for which removal shall be registered. */ - public void removeAll(String key, Collection removedObjects) { - for (ITransactionObject removed : removedObjects) { + public void removeAll(String key, Collection removedObjects) { + for (Object removed : removedObjects) { remove(key, removed); } } @@ -366,7 +334,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for addition */ - public void add(ITransactionObject object) { + public void add(Object object) { add(object.getClass().getName(), object); } @@ -376,7 +344,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for updating */ - public void update(ITransactionObject object) { + public void update(Object object) { update(object.getClass().getName(), object); } @@ -386,7 +354,7 @@ public class ObjectFilter { * @param object * The object that shall be registered for removal */ - public void remove(ITransactionObject object) { + public void remove(Object object) { remove(object.getClass().getName(), object); } @@ -397,8 +365,8 @@ public class ObjectFilter { * @param objects * The objects that shall be registered for addition */ - public void addAll(List objects) { - for (ITransactionObject addedObj : objects) { + public void addAll(List objects) { + for (Object addedObj : objects) { add(addedObj.getClass().getName(), addedObj); } } @@ -410,8 +378,8 @@ public class ObjectFilter { * @param updateObjects * The objects that shall be registered for updating */ - public void updateAll(List updateObjects) { - for (ITransactionObject update : updateObjects) { + public void updateAll(List updateObjects) { + for (Object update : updateObjects) { update(update.getClass().getName(), update); } } @@ -423,8 +391,8 @@ public class ObjectFilter { * @param removedObjects * The objects that shall be registered for removal */ - public void removeAll(List removedObjects) { - for (ITransactionObject removed : removedObjects) { + public void removeAll(List removedObjects) { + for (Object removed : removedObjects) { remove(removed.getClass().getName(), removed); } } @@ -436,8 +404,8 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of all objects registered under the given key and that need to be added. */ - public List getAdded(String key) { - List addedObjects = new LinkedList(); + public List getAdded(String key) { + List addedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { @@ -456,7 +424,7 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of all objects registered under the given key and that need to be added. */ - public List getAdded(Class clazz, String key) { + public List getAdded(Class clazz, String key) { List addedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { @@ -478,8 +446,8 @@ public class ObjectFilter { * registration key of the objects to match * @return The list of all objects registered under the given key and that need to be updated. */ - public List getUpdated(String key) { - List updatedObjects = new LinkedList(); + public List getUpdated(String key) { + List updatedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { @@ -496,7 +464,7 @@ public class ObjectFilter { * registration key of the objects to match * @return The list of all objects registered under the given key and that need to be updated. */ - public List getUpdated(Class clazz, String key) { + public List getUpdated(Class clazz, String key) { List updatedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { @@ -518,8 +486,8 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of object registered under the given key that have, as a final action, removal. */ - public List getRemoved(String key) { - List removedObjects = new LinkedList(); + public List getRemoved(String key) { + List removedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { @@ -536,7 +504,7 @@ public class ObjectFilter { * The registration key of the objects to match * @return The list of object registered under the given key that have, as a final action, removal. */ - public List getRemoved(Class clazz, String key) { + public List getRemoved(Class clazz, String key) { List removedObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { @@ -559,8 +527,8 @@ public class ObjectFilter { * The registration key for which the objects shall be retrieved * @return The list of objects matching the given key. */ - public List getAll(String key) { - List allObjects = new LinkedList(); + public List getAll(String key) { + List allObjects = new LinkedList(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { @@ -606,6 +574,14 @@ public class ObjectFilter { this.keySet.clear(); } + public int sizeKeySet() { + return this.keySet.size(); + } + + public int sizeCache() { + return this.cache.size(); + } + /** * @return get a unique transaction ID */ diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java new file mode 100644 index 000000000..5ae6b64b4 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2012 + * + * This file is part of ??????????????? + * + * ?????????????? is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privilege is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ????????????????. If not, see . + * + */ +package ch.eitchnet.utils.objectfilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +/** + * @author Robert von Burg + * + */ +public class ObjectFilterTest { + + @Test + public void shouldAdd() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj); + + testAssertions(filter, 1, 1, 1, 0, 0); + } + + @Test + public void shouldUpdate() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.update(myObj); + + testAssertions(filter, 1, 1, 0, 1, 0); + } + + @Test + public void shouldRemove() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(myObj); + + testAssertions(filter, 1, 1, 0, 0, 1); + } + + @Test + public void shouldAddUpdateRemoveDifferentObjects() { + + Object objToAdd = new Object(); + Object objToUpdate = new Object(); + Object objToRemove = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(objToAdd); + filter.update(objToUpdate); + filter.remove(objToRemove); + + testAssertions(filter, 3, 1, 1, 1, 1); + } + + @Test + public void shouldAddUpdateRemoveSameObject() { + + Object objToAddUpdateRemove = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(objToAddUpdateRemove); + filter.update(objToAddUpdateRemove); + filter.remove(objToAddUpdateRemove); + + testAssertions(filter, 0, 1, 0, 0, 0); + } + + @Test + public void shouldNotAddTwice() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj); + + try { + filter.add(myObj); + fail("Should have failed adding twice!"); + } catch (RuntimeException e) { + assertEquals("Stale State exception: Invalid + after +", e.getMessage()); + } + + testAssertions(filter, 1, 1, 1, 0, 0); + } + + @Test + public void shouldNotRemoveTwice() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(myObj); + + try { + filter.remove(myObj); + fail("Should have failed removing twice!"); + } catch (RuntimeException e) { + assertEquals("Stale State exception: Invalid - after -", e.getMessage()); + } + + testAssertions(filter, 1, 1, 0, 0, 1); + } + + @Test + public void shouldAcceptUpdateTwice() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.update(myObj); + filter.update(myObj); + testAssertions(filter, 1, 1, 0, 1, 0); + } + + @Test + public void shouldStillBeAddWithUpdate() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj); + filter.update(myObj); + filter.update(myObj); + testAssertions(filter, 1, 1, 1, 0, 0); + } + + @Test + public void shouldNotAcceptAddAfterModify() { + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.update(myObj); + + try { + filter.add(myObj); + fail("Should have failed add after modify"); + } catch (RuntimeException e) { + assertEquals("Stale State exception: Invalid + after +=", e.getMessage()); + } + + testAssertions(filter, 1, 1, 0, 1, 0); + } + + @Test + public void shouldAcceptAddAfterRemove() { + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(myObj); + filter.add(myObj); + + testAssertions(filter, 1, 1, 0, 1, 0); + } + + @Test + public void shouldNotAcceptModifyAfterRemove() { + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(myObj); + + try { + filter.update(myObj); + fail("Should have failed modify after remove"); + } catch (RuntimeException e) { + assertEquals("Stale State exception: Invalid += after -", e.getMessage()); + } + + testAssertions(filter, 1, 1, 0, 0, 1); + } + + @Test + public void shouldNotAcceptDifferentKeyForSameObject() { + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj); + + try { + filter.update("different_key", myObj); + fail("Should have failed because of different key for already registered object"); + } catch (RuntimeException e) { + String msg = "Invalid key provided for object with transaction ID -1 and operation MODIFY: existing key is java.lang.Object, new key is different_key. Object may be present in the same filter instance only once, registered using one key only. Object"; + assertTrue("Encountered exception: " + e.getMessage(), e.getMessage().contains(msg)); + } + + testAssertions(filter, 1, 1, 1, 0, 0); + } + + @Test + public void shouldReplaceInstanceIfObjectIsEqual() { + fail("Not yet implemented"); + } + + @Test + public void shouldRemoveAfterAddAndRemove() { + + Object myObj = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj); + filter.remove(myObj); + testAssertions(filter, 0, 1, 0, 0, 0); + } + + private void testAssertions(ObjectFilter filter, int size, int sizeKeySet, int added, int updated, int removed) { + assertEquals(size, filter.sizeCache()); + assertEquals(sizeKeySet, filter.sizeKeySet()); + + List addedList = filter.getAdded(Object.class.getName()); + assertEquals(added, addedList.size()); + + List updatedList = filter.getUpdated(Object.class.getName()); + assertEquals(updated, updatedList.size()); + + List removedList = filter.getRemoved(Object.class.getName()); + assertEquals(removed, removedList.size()); + } +} From f53c20d5151a3739ceede44b3641c92349b1baf2 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 12 Aug 2013 11:02:02 +0200 Subject: [PATCH 033/180] extended XmlHelper to take InputStream and always use \n as line sep --- .../ch/eitchnet/utils/helper/XmlHelper.java | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index d9a0c3af9..211004c3e 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -20,7 +20,10 @@ package ch.eitchnet.utils.helper; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -51,6 +54,16 @@ import ch.eitchnet.utils.exceptions.XmlException; */ public class XmlHelper { + /** + * PROP_LINE_SEPARATOR = "line.separator" : the system property to fetch defined line separator + */ + public static final String PROP_LINE_SEPARATOR = "line.separator"; + + /** + * UNIX_LINE_SEP = "\n" : mostly we want this line separator, instead of the windows version + */ + public static final String UNIX_LINE_SEP = "\n"; + /** * DEFAULT_ENCODING = "UTF-8" : defines the default UTF-8 encoding expected of XML files */ @@ -63,25 +76,38 @@ public class XmlHelper { * * @param xmlFile * the {@link File} which has the path to the XML file to read - * - * @return a {@link Document} object containing the {@link Element}s of the XML file */ public static void parseDocument(File xmlFile, DefaultHandler xmlHandler) { + try { + XmlHelper.logger.info("Parsing XML document " + xmlFile.getAbsolutePath()); + parseDocument(new FileInputStream(xmlFile), xmlHandler); + } catch (FileNotFoundException e) { + throw new XmlException("The XML file could not be read: " + xmlFile.getAbsolutePath(), e); + } + } + + /** + * Parses an XML file on the file system and returns the resulting {@link Document} object + * + * @param xmlFileInputStream + * the XML {@link InputStream} which is to be parsed + */ + public static void parseDocument(InputStream xmlFileInputStream, DefaultHandler xmlHandler) { + try { SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser sp = spf.newSAXParser(); - XmlHelper.logger.info("Parsing XML document " + xmlFile.getAbsolutePath()); - sp.parse(xmlFile, xmlHandler); + sp.parse(xmlFileInputStream, xmlHandler); } catch (ParserConfigurationException e) { - throw new XmlException("Failed to initialize a SAX Parser: " + e.getLocalizedMessage(), e); + throw new XmlException("Failed to initialize a SAX Parser: " + e.getMessage(), e); } catch (SAXException e) { - throw new XmlException("The XML file " + xmlFile.getAbsolutePath() + " is not parseable:", e); + throw new XmlException("The XML stream is not parseable: " + e.getMessage(), e); } catch (IOException e) { - throw new XmlException("The XML could not be read: " + xmlFile.getAbsolutePath()); + throw new XmlException("The XML stream not be read: " + e.getMessage(), e); } } @@ -100,13 +126,20 @@ public class XmlHelper { XmlHelper.logger.info("Exporting document element " + document.getNodeName() + " to " + file.getAbsolutePath()); + String lineSep = System.getProperty(PROP_LINE_SEPARATOR); try { String encoding = document.getInputEncoding(); if (encoding == null || encoding.isEmpty()) { + XmlHelper.logger.info("No encoding passed. Using default encoding " + XmlHelper.DEFAULT_ENCODING); encoding = XmlHelper.DEFAULT_ENCODING; } + if (!lineSep.equals("\n")) { + XmlHelper.logger.info("Overriding line separator to \\n"); + System.setProperty(PROP_LINE_SEPARATOR, UNIX_LINE_SEP); + } + // Set up a transformer TransformerFactory transfac = TransformerFactory.newInstance(); Transformer transformer = transfac.newTransformer(); @@ -115,7 +148,7 @@ public class XmlHelper { transformer.setOutputProperty(OutputKeys.METHOD, "xml"); transformer.setOutputProperty(OutputKeys.ENCODING, encoding); transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); - //transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", "\t"); + // transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", "\t"); // Transform to file StreamResult result = new StreamResult(file); @@ -126,6 +159,9 @@ public class XmlHelper { throw new XmlException("Exception while exporting to file: " + e, e); + } finally { + + System.setProperty(PROP_LINE_SEPARATOR, lineSep); } } From 1430c9217b215a64a70cae4c2e5f4f86ec532d68 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 12 Aug 2013 11:54:57 +0200 Subject: [PATCH 034/180] Fixed remaining failing tests by implementing the replacing of objects in the filter as required --- .../utils/objectfilter/ObjectFilter.java | 44 +++- .../utils/objectfilter/ObjectFilterTest.java | 204 +++++++++++++++++- 2 files changed, 231 insertions(+), 17 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index 7ee255595..def9671a8 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -118,9 +118,6 @@ public class ObjectFilter { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("add object " + objectToAdd + " with key " + key); - // add the key to the set - this.keySet.add(key); - // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToAdd); if (cached == null) { @@ -148,11 +145,30 @@ public class ObjectFilter { case MODIFY: throw new IllegalStateException("Stale State exception: Invalid + after +="); case REMOVE: + // replace key if necessary + replaceKey(cached.getObject(), objectToAdd); + + // update operation's object cached.setObject(objectToAdd); cached.setOperation(Operation.MODIFY); break; } // switch }// else of object not in cache + + // register the key + this.keySet.add(key); + } + + private void replaceKey(Object oldObject, Object newObject) { + if (oldObject != newObject) { + if (ObjectFilter.logger.isDebugEnabled()) { + String msg = "Replacing key for object as they are not the same reference: old: {0} / new: {1}"; + msg = MessageFormat.format(msg, oldObject, newObject); + ObjectFilter.logger.warn(msg); + } + ObjectCache objectCache = this.cache.remove(oldObject); + this.cache.put(newObject, objectCache); + } } /** @@ -184,9 +200,6 @@ public class ObjectFilter { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("update object " + objectToUpdate + " with key " + key); - // add the key to the keyset - this.keySet.add(key); - // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToUpdate); if (cached == null) { @@ -209,15 +222,20 @@ public class ObjectFilter { Operation op = cached.getOperation(); switch (op) { case ADD: - cached.setObject(objectToUpdate); - break; case MODIFY: + // replace key if necessary + replaceKey(cached.getObject(), objectToUpdate); + + // update operation's object cached.setObject(objectToUpdate); break; case REMOVE: throw new IllegalStateException("Stale State exception: Invalid += after -"); } // switch }// else of object not in cache + + // register the key + this.keySet.add(key); } /** @@ -249,9 +267,6 @@ public class ObjectFilter { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug("remove object " + objectToRemove + " with key " + key); - // add the key to the keyset - this.keySet.add(key); - // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToRemove); if (cached == null) { @@ -277,6 +292,10 @@ public class ObjectFilter { this.cache.remove(objectToRemove); break; case MODIFY: + // replace key if necessary + replaceKey(cached.getObject(), objectToRemove); + + // update operation's object cached.setObject(objectToRemove); cached.setOperation(Operation.REMOVE); break; @@ -284,6 +303,9 @@ public class ObjectFilter { throw new IllegalStateException("Stale State exception: Invalid - after -"); } // switch } + + // register the key + this.keySet.add(key); } /** diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java index 5ae6b64b4..ef30ddee4 100644 --- a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -19,13 +19,14 @@ */ package ch.eitchnet.utils.objectfilter; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.*; - import java.util.List; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + /** * @author Robert von Burg * @@ -208,7 +209,7 @@ public class ObjectFilterTest { filter.update("different_key", myObj); fail("Should have failed because of different key for already registered object"); } catch (RuntimeException e) { - String msg = "Invalid key provided for object with transaction ID -1 and operation MODIFY: existing key is java.lang.Object, new key is different_key. Object may be present in the same filter instance only once, registered using one key only. Object"; + String msg = "Object may be present in the same filter instance only once, registered using one key only"; assertTrue("Encountered exception: " + e.getMessage(), e.getMessage().contains(msg)); } @@ -216,8 +217,79 @@ public class ObjectFilterTest { } @Test - public void shouldReplaceInstanceIfObjectIsEqual() { - fail("Not yet implemented"); + public void shouldReplaceOnAddAfterRemove() { + + TestObject obj1 = new TestObject(1); + TestObject obj2 = new TestObject(1); + assertEquals("Test objects are not equal!", obj1, obj2); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(Object.class.getName(), obj1); + filter.add(Object.class.getName(), obj2); + + testAssertions(filter, 1, 1, 0, 1, 0); + + List updated = filter.getUpdated(Object.class.getName()); + Object updatedObj = updated.get(0); + String msg = "registered object is not the last operation's object"; + assertTrue(msg, obj2 == updatedObj); + } + + @Test + public void shouldReplaceOnUpdateAfterAdd() { + + TestObject obj1 = new TestObject(1); + TestObject obj2 = new TestObject(1); + assertEquals("Test objects are not equal!", obj1, obj2); + + ObjectFilter filter = new ObjectFilter(); + filter.add(Object.class.getName(), obj1); + filter.update(Object.class.getName(), obj2); + + testAssertions(filter, 1, 1, 1, 0, 0); + + List added = filter.getAdded(Object.class.getName()); + Object addedObj = added.get(0); + String msg = "registered object is not the last operation's object"; + assertTrue(msg, obj2 == addedObj); + } + + @Test + public void shouldReplaceOnUpdateAfterUpdate() { + + TestObject obj1 = new TestObject(1); + TestObject obj2 = new TestObject(1); + assertEquals("Test objects are not equal!", obj1, obj2); + + ObjectFilter filter = new ObjectFilter(); + filter.update(Object.class.getName(), obj1); + filter.update(Object.class.getName(), obj2); + + testAssertions(filter, 1, 1, 0, 1, 0); + + List updated = filter.getUpdated(Object.class.getName()); + Object updatedObj = updated.get(0); + String msg = "registered object is not the last operation's object"; + assertTrue(msg, obj2 == updatedObj); + } + + @Test + public void shouldReplaceOnRemoveAfterModify() { + + TestObject obj1 = new TestObject(1); + TestObject obj2 = new TestObject(1); + assertEquals("Test objects are not equal!", obj1, obj2); + + ObjectFilter filter = new ObjectFilter(); + filter.update(Object.class.getName(), obj1); + filter.remove(Object.class.getName(), obj2); + + testAssertions(filter, 1, 1, 0, 0, 1); + + List removed = filter.getRemoved(Object.class.getName()); + Object removedObj = removed.get(0); + String msg = "registered object is not the last operation's object"; + assertTrue(msg, obj2 == removedObj); } @Test @@ -231,6 +303,89 @@ public class ObjectFilterTest { testAssertions(filter, 0, 1, 0, 0, 0); } + @Test + public void shouldClear() { + + Object myObj1 = new Object(); + Object myObj2 = new Object(); + Object myObj3 = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj1); + filter.update(myObj2); + filter.remove(myObj3); + + filter.clearCache(); + + testAssertions(filter, 0, 0, 0, 0, 0); + } + + @Test + public void shouldGetAll() { + + Object myObj1 = new Object(); + Object myObj2 = new Object(); + Object myObj3 = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj1); + filter.update(myObj2); + filter.remove(myObj3); + + testAssertions(filter, 3, 1, 1, 1, 1); + + List all = filter.getAll(Object.class.getName()); + assertEquals(3, all.size()); + assertTrue(all.contains(myObj1)); + assertTrue(all.contains(myObj2)); + assertTrue(all.contains(myObj3)); + } + + @Test + public void shouldGetAdded() { + + Object myObj1 = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.add(myObj1); + + testAssertions(filter, 1, 1, 1, 0, 0); + + List list = filter.getAdded(Object.class.getName()); + assertEquals(1, list.size()); + assertTrue(list.contains(myObj1)); + } + + @Test + public void shouldGetUpdated() { + + Object myObj1 = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.update(myObj1); + + testAssertions(filter, 1, 1, 0, 1, 0); + + List list = filter.getUpdated(Object.class.getName()); + assertEquals(1, list.size()); + assertTrue(list.contains(myObj1)); + } + + @Test + public void shouldGetRemoved() { + + Object myObj1 = new Object(); + + ObjectFilter filter = new ObjectFilter(); + filter.remove(myObj1); + + testAssertions(filter, 1, 1, 0, 0, 1); + + List list = filter.getRemoved(Object.class.getName()); + assertEquals(1, list.size()); + assertTrue(list.contains(myObj1)); + } + private void testAssertions(ObjectFilter filter, int size, int sizeKeySet, int added, int updated, int removed) { assertEquals(size, filter.sizeCache()); assertEquals(sizeKeySet, filter.sizeKeySet()); @@ -244,4 +399,41 @@ public class ObjectFilterTest { List removedList = filter.getRemoved(Object.class.getName()); assertEquals(removed, removedList.size()); } + + private class TestObject { + private int id; + + public TestObject(int id) { + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getOuterType().hashCode(); + result = prime * result + id; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestObject other = (TestObject) obj; + if (!getOuterType().equals(other.getOuterType())) + return false; + if (id != other.id) + return false; + return true; + } + + private ObjectFilterTest getOuterType() { + return ObjectFilterTest.this; + } + } } From 984ae06df6de72db9e77f705e694d7c441393a34 Mon Sep 17 00:00:00 2001 From: "U-PX\\rvb" Date: Tue, 20 Aug 2013 09:06:55 +0200 Subject: [PATCH 035/180] [Project] modified pom.xml to also generate javadocs and also deploy those to nexus --- pom.xml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index ed75a6af3..456a807fb 100644 --- a/pom.xml +++ b/pom.xml @@ -135,14 +135,25 @@ attach-sources - verify + package - jar-no-fork + jar - + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + deploy + jar + + + org.apache.maven.plugins maven-jar-plugin @@ -156,7 +167,6 @@ - org.apache.maven.plugins maven-site-plugin @@ -164,6 +174,19 @@ UTF-8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + + deploy + deploy + deploy + + From 1c075554483107f8b287906786d8b82b432ae485 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 20 Aug 2013 18:45:18 +0200 Subject: [PATCH 036/180] [Minor] small code cleanup added default constructor, instead of instantiating instance variables at declaration --- .../eitchnet/utils/objectfilter/ObjectFilter.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index def9671a8..bd9cd4ff2 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -82,11 +82,19 @@ public class ObjectFilter { private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class); - private HashMap cache = new HashMap(); - private HashSet keySet = new HashSet(); - private static long id = ObjectCache.UNSET; + private final HashMap cache; + private final HashSet keySet; + + /** + * Default constructor initializing the filter + */ + public ObjectFilter() { + this.cache = new HashMap(); + this.keySet = new HashSet(); + } + /** * Register, under the given key, the addition of the given object. *

From 209a22033175b8764708ffefdb11842915fef45c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 20 Aug 2013 18:46:46 +0200 Subject: [PATCH 037/180] [Major] Moved property helper methods to PropertiesHelper Moved the property helper methods from SystemHelper to a new class PropertiesHelper and added methods where one can pass in the actual properties thus not being restricted to the system properties --- .../utils/helper/PropertiesHelper.java | 190 ++++++++++++++++++ .../eitchnet/utils/helper/SystemHelper.java | 64 ------ .../ch/eitchnet/utils/helper/XmlHelper.java | 4 +- 3 files changed, 192 insertions(+), 66 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java new file mode 100644 index 000000000..1c4fcf31a --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java @@ -0,0 +1,190 @@ +/* + * 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.utils.helper; + +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) + throw new RuntimeException("[" + context + "] Property " + key + " is not set, and no default was given!"); + + 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/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index fbf925bca..57dad260d 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -115,68 +115,4 @@ public class SystemHelper { return "Memory available " + SystemHelper.getMaxMemory() + " / Used: " + SystemHelper.getUsedMemory() + " / Free:" + SystemHelper.getFreeMemory(); } - - /** - * 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 { - String property = System.getProperty(key, def); - if (property == null) - throw new RuntimeException("[" + context + "] Property " + key + " is not set, and no default was given!"); - - return property; - } - - /** - * 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(SystemHelper.getProperty(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(SystemHelper.getProperty(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(SystemHelper.getProperty(context, key, def == null ? null : def.toString())); - } } diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index 211004c3e..aa38e70f3 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -65,9 +65,9 @@ public class XmlHelper { public static final String UNIX_LINE_SEP = "\n"; /** - * DEFAULT_ENCODING = "UTF-8" : defines the default UTF-8 encoding expected of XML files + * DEFAULT_ENCODING = "utf-8" : defines the default UTF-8 encoding expected of XML files */ - public static final String DEFAULT_ENCODING = "UTF-8"; + public static final String DEFAULT_ENCODING = "utf-8"; private static final Logger logger = LoggerFactory.getLogger(XmlHelper.class); From c9cbb534755f02a9764e89a071b395d627941c01 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 15 Sep 2013 21:44:50 +0200 Subject: [PATCH 038/180] [Minor] changed map declaration to be its interface --- .../java/ch/eitchnet/utils/objectfilter/ObjectFilter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index bd9cd4ff2..9eacf63f8 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import org.slf4j.Logger; @@ -84,8 +85,8 @@ public class ObjectFilter { private static long id = ObjectCache.UNSET; - private final HashMap cache; - private final HashSet keySet; + private final Map cache; + private final Set keySet; /** * Default constructor initializing the filter From e6f8343af77c725c5d5f3d880edbd7672974ea0b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 15 Sep 2013 21:45:40 +0200 Subject: [PATCH 039/180] [Minor] fixed compiler warnings --- .../utils/objectfilter/ObjectFilterTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java index ef30ddee4..ea0b7e6f9 100644 --- a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -19,14 +19,14 @@ */ package ch.eitchnet.utils.objectfilter; -import java.util.List; - -import org.junit.Test; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.List; + +import org.junit.Test; + /** * @author Robert von Burg * @@ -412,7 +412,7 @@ public class ObjectFilterTest { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); - result = prime * result + id; + result = prime * result + this.id; return result; } @@ -427,7 +427,7 @@ public class ObjectFilterTest { TestObject other = (TestObject) obj; if (!getOuterType().equals(other.getOuterType())) return false; - if (id != other.id) + if (this.id != other.id) return false; return true; } From daf34735ae00c60738c761293cdc2be4fa1068c0 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 16 Sep 2013 07:46:57 +0200 Subject: [PATCH 040/180] [Project] changed Java version frm 1.6 to 1.7 in pom.xml --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 456a807fb..1806f64b9 100644 --- a/pom.xml +++ b/pom.xml @@ -124,8 +124,8 @@ maven-compiler-plugin 3.0 - 1.6 - 1.6 + 1.7 + 1.7 From a5ffe0a6cc4022b85331b207e3e6a882486f8482 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 21 Sep 2013 09:11:40 +0200 Subject: [PATCH 041/180] [Project] moved some project details to parent ch.eitchnet.parent This simplifies project setup and maintenance --- pom.xml | 140 +++----------------------------------------------------- 1 file changed, 7 insertions(+), 133 deletions(-) diff --git a/pom.xml b/pom.xml index 1806f64b9..03a84e647 100644 --- a/pom.xml +++ b/pom.xml @@ -2,110 +2,33 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - ch.eitchnet + + ch.eitchnet + ch.eitchnet.parent + 0.0.1-SNAPSHOT + ../ch.eitchnet.parent/pom.xml + + ch.eitchnet.utils jar 0.2.0-SNAPSHOT ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse https://github.com/eitch/ch.eitchnet.utils - - - UTF-8 - - - - 2011 - - - GNU Lesser General Public License - http://www.gnu.org/licenses/lgpl.html - repo - - - - eitchnet.ch - http://blog.eitchnet.ch - - - - eitch - Robert von Vurg - eitch@eitchnet.ch - http://blog.eitchnet.ch - eitchnet.ch - http://blog.eitchnet.ch - - architect - developer - - +1 - - http://localhost - - - Github Issues https://github.com/eitch/ch.eitchnet.utils/issues - - scm:git:https://github.com/eitch/ch.eitchnet.utils.git scm:git:git@github.com:eitch/ch.eitchnet.utils.git https://github.com/eitch/ch.eitchnet.utils - - - - - deployment - Internal Releases - http://nexus.eitchnet.ch/content/repositories/releases/ - - - deployment - Internal Releases - http://nexus.eitchnet.ch/content/repositories/snapshots/ - - - - - junit - junit - 4.10 - test - - - org.slf4j - slf4j-api - 1.7.2 - - - org.slf4j - slf4j-log4j12 - 1.7.2 - test - @@ -113,80 +36,31 @@ org.apache.maven.plugins maven-eclipse-plugin - 2.9 - - true - true - org.apache.maven.plugins maven-compiler-plugin - 3.0 - - 1.7 - 1.7 - org.apache.maven.plugins maven-source-plugin - 2.1.2 - - - attach-sources - package - - jar - - - org.apache.maven.plugins maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - deploy - jar - - org.apache.maven.plugins maven-jar-plugin - 2.4 - - - - true - true - - - org.apache.maven.plugins maven-site-plugin - 2.3 - - UTF-8 - org.apache.maven.plugins maven-deploy-plugin - 2.7 - - - deploy - deploy - deploy - - From b7a4762a059da9dd04bfd6677a29ce61b3f7b12d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 21 Sep 2013 10:03:27 +0200 Subject: [PATCH 042/180] [Minor] Made Base encoding/decoding performance tests configurable To enable them, set the system property ch.eitchnet.utils.test.runPerfTests=true --- .../utils/helper/BaseDecodingTest.java | 116 +++++++++++++----- .../utils/helper/BaseEncodingTest.java | 109 +++++++++++----- 2 files changed, 157 insertions(+), 68 deletions(-) diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java index 78a9ea566..fee6c668e 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -30,6 +30,7 @@ import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import junit.framework.Assert; import org.junit.Test; +import org.junit.runners.JUnit4; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,8 +39,16 @@ import org.slf4j.LoggerFactory; * */ public class BaseDecodingTest { + public static final String PROP_RUN_PERF_TESTS = "ch.eitchnet.utils.test.runPerfTests"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(BaseDecodingTest.class); + public static boolean isSkipPerfTests() { + String context = BaseDecodingTest.class.getSimpleName(); + String key = PROP_RUN_PERF_TESTS; + boolean runPerfTests = PropertiesHelper.getPropertyBool(context, key, Boolean.FALSE); + return !runPerfTests; + } + @Test public void testBase64() { Assert.assertEquals("", fromBase64("")); @@ -49,6 +58,59 @@ public class BaseDecodingTest { Assert.assertEquals("foob", fromBase64("Zm9vYg==")); Assert.assertEquals("fooba", fromBase64("Zm9vYmE=")); Assert.assertEquals("foobar", fromBase64("Zm9vYmFy")); + } + + @Test + public void testBase32() { + Assert.assertEquals("", fromBase32("")); + Assert.assertEquals("f", fromBase32("MY======")); + Assert.assertEquals("fo", fromBase32("MZXQ====")); + Assert.assertEquals("foo", fromBase32("MZXW6===")); + Assert.assertEquals("foob", fromBase32("MZXW6YQ=")); + Assert.assertEquals("fooba", fromBase32("MZXW6YTB")); + Assert.assertEquals("foobar", fromBase32("MZXW6YTBOI======")); + } + + @Test + public void testBase32Hex() { + Assert.assertEquals("", fromBase32Hex("")); + Assert.assertEquals("f", fromBase32Hex("CO======")); + Assert.assertEquals("fo", fromBase32Hex("CPNG====")); + Assert.assertEquals("foo", fromBase32Hex("CPNMU===")); + Assert.assertEquals("foob", fromBase32Hex("CPNMUOG=")); + Assert.assertEquals("fooba", fromBase32Hex("CPNMUOJ1")); + Assert.assertEquals("foobar", fromBase32Hex("CPNMUOJ1E8======")); + } + + @Test + public void testBase32Dmedia() { + + Assert.assertEquals("", fromBase32Dmedia("")); + Assert.assertEquals("binary foo", fromBase32Dmedia("FCNPVRELI7J9FUUI")); + Assert.assertEquals("f", fromBase32Dmedia("FR======")); + Assert.assertEquals("fo", fromBase32Dmedia("FSQJ====")); + Assert.assertEquals("foo", fromBase32Dmedia("FSQPX===")); + Assert.assertEquals("foob", fromBase32Dmedia("FSQPXRJ=")); + Assert.assertEquals("fooba", fromBase32Dmedia("FSQPXRM4")); + Assert.assertEquals("foobar", fromBase32Dmedia("FSQPXRM4HB======")); + } + + @Test + public void testBase16() { + Assert.assertEquals("", fromBase16("")); + Assert.assertEquals("f", fromBase16("66")); + Assert.assertEquals("fo", fromBase16("666F")); + Assert.assertEquals("foo", fromBase16("666F6F")); + Assert.assertEquals("foob", fromBase16("666F6F62")); + Assert.assertEquals("fooba", fromBase16("666F6F6261")); + Assert.assertEquals("foobar", fromBase16("666F6F626172")); + } + + @Test + public void testBase64Perf() { + if (isSkipPerfTests()) { + return; + } byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -63,14 +125,11 @@ public class BaseDecodingTest { } @Test - public void testBase32() { - Assert.assertEquals("", fromBase32("")); - Assert.assertEquals("f", fromBase32("MY======")); - Assert.assertEquals("fo", fromBase32("MZXQ====")); - Assert.assertEquals("foo", fromBase32("MZXW6===")); - Assert.assertEquals("foob", fromBase32("MZXW6YQ=")); - Assert.assertEquals("fooba", fromBase32("MZXW6YTB")); - Assert.assertEquals("foobar", fromBase32("MZXW6YTBOI======")); + public void testBase32Perf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -85,14 +144,11 @@ public class BaseDecodingTest { } @Test - public void testBase32Hex() { - Assert.assertEquals("", fromBase32Hex("")); - Assert.assertEquals("f", fromBase32Hex("CO======")); - Assert.assertEquals("fo", fromBase32Hex("CPNG====")); - Assert.assertEquals("foo", fromBase32Hex("CPNMU===")); - Assert.assertEquals("foob", fromBase32Hex("CPNMUOG=")); - Assert.assertEquals("fooba", fromBase32Hex("CPNMUOJ1")); - Assert.assertEquals("foobar", fromBase32Hex("CPNMUOJ1E8======")); + public void testBase32HexPerf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { @@ -107,16 +163,11 @@ public class BaseDecodingTest { } @Test - public void testBase32Dmedia() { - - Assert.assertEquals("", fromBase32Dmedia("")); - Assert.assertEquals("binary foo", fromBase32Dmedia("FCNPVRELI7J9FUUI")); - Assert.assertEquals("f", fromBase32Dmedia("FR======")); - Assert.assertEquals("fo", fromBase32Dmedia("FSQJ====")); - Assert.assertEquals("foo", fromBase32Dmedia("FSQPX===")); - Assert.assertEquals("foob", fromBase32Dmedia("FSQPXRJ=")); - Assert.assertEquals("fooba", fromBase32Dmedia("FSQPXRM4")); - Assert.assertEquals("foobar", fromBase32Dmedia("FSQPXRM4HB======")); + public void testBase32DmediaPerf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; @@ -128,14 +179,11 @@ public class BaseDecodingTest { } @Test - public void testBase16() { - Assert.assertEquals("", fromBase16("")); - Assert.assertEquals("f", fromBase16("66")); - Assert.assertEquals("fo", fromBase16("666F")); - Assert.assertEquals("foo", fromBase16("666F6F")); - Assert.assertEquals("foob", fromBase16("666F6F62")); - Assert.assertEquals("fooba", fromBase16("666F6F6261")); - Assert.assertEquals("foobar", fromBase16("666F6F626172")); + public void testBase16Perf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } byte[] bytes = new byte[1024 * 1024]; for (int i = 0; i < bytes.length; i++) { diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index 7a2b3052f..7bca92e75 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -21,10 +21,12 @@ */ package ch.eitchnet.utils.helper; +import static ch.eitchnet.utils.helper.BaseDecodingTest.PROP_RUN_PERF_TESTS; +import static ch.eitchnet.utils.helper.BaseDecodingTest.isSkipPerfTests; import static ch.eitchnet.utils.helper.BaseEncoding.toBase16; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32; -import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Dmedia; +import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import static ch.eitchnet.utils.helper.BaseEncoding.toBase64; import junit.framework.Assert; @@ -49,14 +51,6 @@ public class BaseEncodingTest { Assert.assertEquals("Zm9vYg==", toBase64("foob")); Assert.assertEquals("Zm9vYmE=", toBase64("fooba")); Assert.assertEquals("Zm9vYmFy", toBase64("foobar")); - - long start = System.nanoTime(); - byte[] bytes = new byte[1024 * 1024]; - for (int i = 0; i < 200; i++) { - toBase64(bytes); - } - long end = System.nanoTime(); - logger.info("Encoding 200MB Base64 took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -68,14 +62,6 @@ public class BaseEncodingTest { Assert.assertEquals("MZXW6YQ=", toBase32("foob")); Assert.assertEquals("MZXW6YTB", toBase32("fooba")); Assert.assertEquals("MZXW6YTBOI======", toBase32("foobar")); - - long start = System.nanoTime(); - byte[] bytes = new byte[1024 * 1024]; - for (int i = 0; i < 200; i++) { - toBase32(bytes); - } - long end = System.nanoTime(); - logger.info("Encoding 200MB Base32 took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -87,19 +73,10 @@ public class BaseEncodingTest { Assert.assertEquals("CPNMUOG=", toBase32Hex("foob")); Assert.assertEquals("CPNMUOJ1", toBase32Hex("fooba")); Assert.assertEquals("CPNMUOJ1E8======", toBase32Hex("foobar")); - - long start = System.nanoTime(); - byte[] bytes = new byte[1024 * 1024]; - for (int i = 0; i < 200; i++) { - toBase32Hex(bytes); - } - long end = System.nanoTime(); - logger.info("Encoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); } @Test public void testBase32Dmedia() { - Assert.assertEquals("", toBase32Dmedia("")); Assert.assertEquals("FCNPVRELI7J9FUUI", toBase32Dmedia("binary foo")); Assert.assertEquals("FR======", toBase32Dmedia("f")); @@ -108,14 +85,6 @@ public class BaseEncodingTest { Assert.assertEquals("FSQPXRJ=", toBase32Dmedia("foob")); Assert.assertEquals("FSQPXRM4", toBase32Dmedia("fooba")); Assert.assertEquals("FSQPXRM4HB======", toBase32Dmedia("foobar")); - - long start = System.nanoTime(); - byte[] bytes = new byte[1024 * 1024]; - for (int i = 0; i < 200; i++) { - toBase32Hex(bytes); - } - long end = System.nanoTime(); - logger.info("Encoding 200MB Base32Dmedia took " + StringHelper.formatNanoDuration(end - start)); } @Test @@ -127,6 +96,78 @@ public class BaseEncodingTest { Assert.assertEquals("666F6F62", toBase16("foob")); Assert.assertEquals("666F6F6261", toBase16("fooba")); Assert.assertEquals("666F6F626172", toBase16("foobar")); + } + + @Test + public void testBase64Perf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase64(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base64 took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase32Perf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32 took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase32HexPerf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32Hex took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase32DmediaPerf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } + + long start = System.nanoTime(); + byte[] bytes = new byte[1024 * 1024]; + for (int i = 0; i < 200; i++) { + toBase32Hex(bytes); + } + long end = System.nanoTime(); + logger.info("Encoding 200MB Base32Dmedia took " + StringHelper.formatNanoDuration(end - start)); + } + + @Test + public void testBase16Perf() { + if (isSkipPerfTests()) { + logger.info("Not running performance tests as not enabled by system property " + PROP_RUN_PERF_TESTS); + return; + } long start = System.nanoTime(); byte[] bytes = new byte[1024 * 1024]; From 41a7198cdd9965b3290d4ba130e06bf913b2fd2d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 8 Oct 2013 18:57:27 +0200 Subject: [PATCH 043/180] [New] added newline constant to StringHelper --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index c6cb8841b..4646fd8e9 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -34,6 +34,8 @@ import org.slf4j.LoggerFactory; */ public class StringHelper { + public static final String NEW_LINE = "\n"; //$NON-NLS-1$ + private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); /** From 601692d3d9b70520c870383397925334838739fc Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 8 Oct 2013 19:57:40 +0200 Subject: [PATCH 044/180] [New] added further constants to StringHelper --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 4646fd8e9..df8f094f4 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -35,6 +35,8 @@ import org.slf4j.LoggerFactory; 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 NULL = "null"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); From 200f2081ec02e5498dd14af2395fcab892873a3c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 8 Oct 2013 22:02:42 +0200 Subject: [PATCH 045/180] [New] added a new ClassHelper utility class --- .../ch/eitchnet/utils/helper/ClassHelper.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/ClassHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java new file mode 100644 index 000000000..bcd1d496d --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java @@ -0,0 +1,24 @@ +package ch.eitchnet.utils.helper; + +import java.text.MessageFormat; + +/** + * Utility class for working with {@link Class Classes} + * + * @author Robert von Burg + */ +public class ClassHelper { + + @SuppressWarnings("unchecked") + public static T instantiateClass(String className) { + + try { + Class clazz = Class.forName(className); + return (T) clazz.newInstance(); + } catch (Exception e) { + String msg = "Failed to load class {0} due to error: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, className, e.getMessage()); + throw new IllegalArgumentException(msg); + } + } +} From f176960ba92c6beca0d8e60127213fc4b34715cc Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 20 Oct 2013 00:17:20 +0200 Subject: [PATCH 046/180] [New] added ObjectFilter.getAll() further informations added ObjectFilter.getAll(Class, String) and ObjectFilter.getAll(Class clazz) --- .../utils/objectfilter/ObjectFilter.java | 96 ++++++++++++++++--- 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index 9eacf63f8..cc0f9123c 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -125,7 +125,7 @@ public class ObjectFilter { public void add(String key, Object objectToAdd) { if (ObjectFilter.logger.isDebugEnabled()) - ObjectFilter.logger.debug("add object " + objectToAdd + " with key " + key); + ObjectFilter.logger.debug(MessageFormat.format("add object {0} with key {1}", objectToAdd, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToAdd); @@ -140,7 +140,7 @@ public class ObjectFilter { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { - String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.ADD.toString(), existingKey, key, objectToAdd.toString())); } @@ -150,9 +150,9 @@ public class ObjectFilter { Operation op = cached.getOperation(); switch (op) { case ADD: - throw new IllegalStateException("Stale State exception: Invalid + after +"); + throw new IllegalStateException("Stale State exception: Invalid + after +"); //$NON-NLS-1$ case MODIFY: - throw new IllegalStateException("Stale State exception: Invalid + after +="); + throw new IllegalStateException("Stale State exception: Invalid + after +="); //$NON-NLS-1$ case REMOVE: // replace key if necessary replaceKey(cached.getObject(), objectToAdd); @@ -161,6 +161,8 @@ public class ObjectFilter { cached.setObject(objectToAdd); cached.setOperation(Operation.MODIFY); break; + default: + throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch }// else of object not in cache @@ -171,7 +173,7 @@ public class ObjectFilter { private void replaceKey(Object oldObject, Object newObject) { if (oldObject != newObject) { if (ObjectFilter.logger.isDebugEnabled()) { - String msg = "Replacing key for object as they are not the same reference: old: {0} / new: {1}"; + String msg = "Replacing key for object as they are not the same reference: old: {0} / new: {1}"; //$NON-NLS-1$ msg = MessageFormat.format(msg, oldObject, newObject); ObjectFilter.logger.warn(msg); } @@ -207,7 +209,7 @@ public class ObjectFilter { public void update(String key, Object objectToUpdate) { if (ObjectFilter.logger.isDebugEnabled()) - ObjectFilter.logger.debug("update object " + objectToUpdate + " with key " + key); + ObjectFilter.logger.debug(MessageFormat.format("update object {0} with key {1}", objectToUpdate, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToUpdate); @@ -222,7 +224,7 @@ public class ObjectFilter { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { - String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.MODIFY.toString(), existingKey, key, objectToUpdate.toString())); } @@ -239,7 +241,9 @@ public class ObjectFilter { cached.setObject(objectToUpdate); break; case REMOVE: - throw new IllegalStateException("Stale State exception: Invalid += after -"); + throw new IllegalStateException("Stale State exception: Invalid += after -"); //$NON-NLS-1$ + default: + throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch }// else of object not in cache @@ -274,7 +278,7 @@ public class ObjectFilter { public void remove(String key, Object objectToRemove) { if (ObjectFilter.logger.isDebugEnabled()) - ObjectFilter.logger.debug("remove object " + objectToRemove + " with key " + key); + ObjectFilter.logger.debug(MessageFormat.format("remove object {0} with key {1}", objectToRemove, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToRemove); @@ -287,7 +291,7 @@ public class ObjectFilter { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { - String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; + String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.REMOVE.toString(), existingKey, key, objectToRemove.toString())); } @@ -309,7 +313,9 @@ public class ObjectFilter { cached.setOperation(Operation.REMOVE); break; case REMOVE: - throw new IllegalStateException("Stale State exception: Invalid - after -"); + throw new IllegalStateException("Stale State exception: Invalid - after -"); //$NON-NLS-1$ + default: + throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch } @@ -433,6 +439,7 @@ public class ObjectFilter { * * @param key * The registration key of the objects to match + * * @return The list of all objects registered under the given key and that need to be added. */ public List getAdded(String key) { @@ -453,6 +460,7 @@ public class ObjectFilter { * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * The registration key of the objects to match + * * @return The list of all objects registered under the given key and that need to be added. */ public List getAdded(Class clazz, String key) { @@ -475,6 +483,7 @@ public class ObjectFilter { * * @param key * registration key of the objects to match + * * @return The list of all objects registered under the given key and that need to be updated. */ public List getUpdated(String key) { @@ -491,8 +500,11 @@ public class ObjectFilter { /** * Get all objects that were registered under the given key and that have as a resulting final action an update. * + * @param clazz + * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * registration key of the objects to match + * * @return The list of all objects registered under the given key and that need to be updated. */ public List getUpdated(Class clazz, String key) { @@ -515,6 +527,7 @@ public class ObjectFilter { * * @param key * The registration key of the objects to match + * * @return The list of object registered under the given key that have, as a final action, removal. */ public List getRemoved(String key) { @@ -531,8 +544,11 @@ public class ObjectFilter { /** * Get all objects that were registered under the given key that have as a resulting final action their removal. * + * @param clazz + * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * The registration key of the objects to match + * * @return The list of object registered under the given key that have, as a final action, removal. */ public List getRemoved(Class clazz, String key) { @@ -550,12 +566,59 @@ public class ObjectFilter { return removedObjects; } + /** + * Get all objects that were registered under the given key + * + * @param clazz + * The class type of the object to be retrieved, that acts as an additional filter criterion. + * @param key + * The registration key of the objects to match + * + * @return The list of object registered under the given key that have, as a final action, removal. + */ + public List getAll(Class clazz, String key) { + List objects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getKey().equals(key)) { + if (objectCache.getObject().getClass() == clazz) { + @SuppressWarnings("unchecked") + V object = (V) objectCache.getObject(); + objects.add(object); + } + } + } + return objects; + } + + /** + * Get all objects that of the given class + * + * @param clazz + * The class type of the object to be retrieved, that acts as an additional filter criterion. + * + * @return The list of all objects that of the given class + */ + public List getAll(Class clazz) { + List objects = new LinkedList(); + Collection allObjs = this.cache.values(); + for (ObjectCache objectCache : allObjs) { + if (objectCache.getObject().getClass() == clazz) { + @SuppressWarnings("unchecked") + V object = (V) objectCache.getObject(); + objects.add(object); + } + } + return objects; + } + /** * Get all the objects that were processed in this transaction, that were registered under the given key. No action * is associated to the object. * * @param key * The registration key for which the objects shall be retrieved + * * @return The list of objects matching the given key. */ public List getAll(String key) { @@ -575,6 +638,7 @@ public class ObjectFilter { * * @param key * The registration key for which the objects shall be retrieved + * * @return The list of objects matching the given key. */ public List getCache(String key) { @@ -598,17 +662,23 @@ public class ObjectFilter { } /** - * Clear the cache. + * Clears the cache */ public void clearCache() { this.cache.clear(); this.keySet.clear(); } + /** + * @return the set of keys used to register objects + */ public int sizeKeySet() { return this.keySet.size(); } + /** + * @return the number of objects registered in this filter + */ public int sizeCache() { return this.cache.size(); } @@ -619,7 +689,7 @@ public class ObjectFilter { public synchronized long dispenseID() { ObjectFilter.id++; if (ObjectFilter.id == Long.MAX_VALUE) { - ObjectFilter.logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); + ObjectFilter.logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); //$NON-NLS-1$ ObjectFilter.id = 1; } return ObjectFilter.id; From 5df4b9edc14342a76e87f5ae7c504f1b5f1a0d68 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 24 Oct 2013 21:43:06 +0200 Subject: [PATCH 047/180] [New] added MathHelper for precision assertions --- .../ch/eitchnet/utils/helper/MathHelper.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/MathHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java new file mode 100644 index 000000000..6f46850b2 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java @@ -0,0 +1,121 @@ +/* + * Created on 05.01.2004 + */ + +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ + +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 msmock, gattom + */ +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); + } +} From 2ccccd6b9ed0ef7543fc3ba098b3a83677da5ee2 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 24 Oct 2013 21:43:16 +0200 Subject: [PATCH 048/180] [New] added IS8601 formatting helpers --- .../ch/eitchnet/utils/iso8601/DateFormat.java | 34 +++ .../utils/iso8601/DurationFormat.java | 34 +++ .../eitchnet/utils/iso8601/FormatFactory.java | 85 ++++++ .../ch/eitchnet/utils/iso8601/ISO8601.java | 264 ++++++++++++++++++ .../utils/iso8601/ISO8601Duration.java | 256 +++++++++++++++++ .../utils/iso8601/ISO8601FormatFactory.java | 72 +++++ .../utils/iso8601/ISO8601Worktime.java | 233 ++++++++++++++++ .../utils/iso8601/WorktimeFormat.java | 34 +++ 8 files changed, 1012 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java create mode 100644 src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java new file mode 100644 index 000000000..d7d918dea --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ + +package ch.eitchnet.utils.iso8601; + +/** + * interface for all date formats internally used by rsp applications + * + * @author msmock + */ +public interface DateFormat { + + /** + * format a long to string + * + * @param l + * @return the formatted string of the long value + */ + public String format(long l); + + /** + * parse a string to long + * + * @param s + * @return the value parsed + */ + public long parse(String s); + +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java new file mode 100644 index 000000000..d7d7c6f00 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ + +package ch.eitchnet.utils.iso8601; + +/** + * interface for all duration formats internally used by the platform + * + * @author msmock + */ +public interface DurationFormat { + + /** + * format a long to string + * + * @param l + * @return formatted string if the long argument + */ + public String format(long l); + + /** + * parse a string to long + * + * @param s + * @return the long value parsed + */ + public long parse(String s); + +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java new file mode 100644 index 000000000..99d43ee76 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java @@ -0,0 +1,85 @@ +package ch.eitchnet.utils.iso8601; + +/** + * This interface defines methods for formatting values for UI representation and also defines factory methods for + * formatters for parsing and formatting duration and date values + * + * @author msmock + */ +public interface FormatFactory { + + /** + * return the formatter for dates + * + * @return RSPDurationFormat + */ + public DateFormat getDateFormat(); + + /** + * return the formatter for durations + * + * @return RSPDurationFormat + */ + public DurationFormat getDurationFormat(); + + /** + * return the formatter for work time + * + * @return RSPWorktimeFormat + */ + public WorktimeFormat getWorktimeFormat(); + + /** + * the date format used in xml import and export + * + * @return RSPDateFormat + */ + public DateFormat getXmlDateFormat(); + + /** + * the duration format used in xml import and export + * + * @return RSPDurationFormat + */ + public DurationFormat getXmlDurationFormat(); + + /** + * Formats a date using {@link #getDateFormat()} + * + * @param date + * the date to format to string + * + * @return String representation of the date + */ + public String formatDate(long date); + + /** + * Formats a duration using {@link #getDateFormat()} + * + * @param duration + * the duration to format to string + * + * @return String representation of the duration + */ + public String formatDuration(long duration); + + /** + * Formats a work time duration using {@link #getDateFormat()} + * + * @param worktime + * the work time duration to format to string + * + * @return String representation of the work time duration + */ + public String formatWorktime(long worktime); + + /** + * Formats a floating point number to have the configured number of decimals + * + * @param value + * the value to format + * + * @return the floating point formatted as a string + */ + public String formatFloat(double value); +} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java new file mode 100644 index 000000000..496b49e96 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -0,0 +1,264 @@ +package ch.eitchnet.utils.iso8601; + +import java.text.DecimalFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.apache.log4j.Logger; + +/** + * + */ +@SuppressWarnings("nls") +public class ISO8601 implements DateFormat { + + private static final Logger logger = Logger.getLogger(ISO8601.class); + + /** + * misc. numeric formats used in formatting + */ + private DecimalFormat xxFormat = new DecimalFormat("00"); + private DecimalFormat xxxFormat = new DecimalFormat("000"); + private DecimalFormat xxxxFormat = new DecimalFormat("0000"); + + /** + * + */ + private Calendar parseToCalendar(String text) { + + if (text == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + // check optional leading sign + char sign; + int start; + if (text.startsWith("-")) { + sign = '-'; + start = 1; + } else if (text.startsWith("+")) { + sign = '+'; + start = 1; + } else { + sign = '+'; // no sign specified, implied '+' + start = 0; + } + + /** + * format of the string is: YYYY-MM-DDThh:mm:ss.SSSTZD + */ + int year, month, day, hour, min, sec, millisec; + String timeZone; + try { + + // year (YYYY) + year = Integer.parseInt(text.substring(start, start + 4)); + start += 4; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + + // month (MM) + month = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + + // day (DD) + day = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter 'T' + if (text.charAt(start) != 'T') { + return null; + } + start++; + + // hour (hh) + hour = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + + // minute (mm) + min = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + + // second (ss) + sec = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + + // delimiter '.' + if (text.charAt(start) == '.') { + start++; + // millisecond (SSS) + millisec = Integer.parseInt(text.substring(start, start + 3)); + start += 3; + } else { + millisec = 0; + } + + if (text.charAt(start) == '+' || text.charAt(start) == '-') { + timeZone = "GMT" + text.substring(start); + } else if (text.substring(start).equals("Z")) { + timeZone = "GMT"; + } else { + return null; + } + + } catch (IndexOutOfBoundsException e) { + return null; + } catch (NumberFormatException e) { + return null; + } + + TimeZone tz = TimeZone.getTimeZone(timeZone); + if (!tz.getID().equals(timeZone)) { + // invalid time zone + return null; + } + + // create Calendar + Calendar cal = Calendar.getInstance(tz); + cal.setLenient(false); + + if (sign == '-' || year == 0) { + // + cal.set(Calendar.YEAR, year + 1); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(Calendar.YEAR, year); + cal.set(Calendar.ERA, GregorianCalendar.AD); + } + + // + cal.set(Calendar.MONTH, month - 1); + cal.set(Calendar.DAY_OF_MONTH, day); + cal.set(Calendar.HOUR_OF_DAY, hour); + cal.set(Calendar.MINUTE, min); + cal.set(Calendar.SECOND, sec); + cal.set(Calendar.MILLISECOND, millisec); + + try { + cal.getTime(); + } catch (IllegalArgumentException e) { + return null; + } + + return cal; + } + + /** + * + */ + private String format(Calendar cal) { + + if (cal == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + // determine era and adjust year if necessary + int year = cal.get(Calendar.YEAR); + if (cal.isSet(Calendar.ERA) && cal.get(Calendar.ERA) == GregorianCalendar.BC) { + /** + * calculate year using astronomical system: year n BCE => astronomical year -n + 1 + */ + year = 0 - year + 1; + } + + /** + * format of date/time string is: YYYY-MM-DDThh:mm:ss.SSSTZD + */ + StringBuilder sWriter = new StringBuilder(); + sWriter.append(this.xxxxFormat.format(year)); + sWriter.append('-'); + sWriter.append(this.xxFormat.format(cal.get(Calendar.MONTH) + 1)); + sWriter.append('-'); + sWriter.append(this.xxFormat.format(cal.get(Calendar.DAY_OF_MONTH))); + sWriter.append('T'); + sWriter.append(this.xxFormat.format(cal.get(Calendar.HOUR_OF_DAY))); + sWriter.append(':'); + sWriter.append(this.xxFormat.format(cal.get(Calendar.MINUTE))); + sWriter.append(':'); + sWriter.append(this.xxFormat.format(cal.get(Calendar.SECOND))); + sWriter.append('.'); + sWriter.append(this.xxxFormat.format(cal.get(Calendar.MILLISECOND))); + TimeZone tz = cal.getTimeZone(); + + int offset = tz.getOffset(cal.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + sWriter.append(offset < 0 ? '-' : '+'); + sWriter.append(this.xxFormat.format(hours)); + sWriter.append(':'); + sWriter.append(this.xxFormat.format(minutes)); + } else { + sWriter.append('Z'); + } + return sWriter.toString(); + } + + /** + * added by msmock convert a long to ISO8601 + * + * @param timePoint + * @return time point as ISO8601 String + */ + @Override + public String format(long timePoint) { + + if (timePoint == Long.MAX_VALUE || timePoint == Long.MIN_VALUE) { + return "-"; + } + + // else + try { + Date date = new Date(); + date.setTime(timePoint); + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return format(cal); + } catch (Exception e) { + logger.error(e, e); + return null; + } + } + + /** + * parse ISO8601 date to long + * + * @param s + * the string to parse + * @return time point as long + * @throws NumberFormatException + */ + @Override + public long parse(String s) { + + if (s.equals("-")) + return Long.MAX_VALUE; + + Calendar cal = parseToCalendar(s); + if (cal != null) { + return cal.getTime().getTime(); + } + + String msg = "Input string " + s + " cannot be parsed to date."; + throw new NumberFormatException(msg); + } +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java new file mode 100644 index 000000000..810b7b34d --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.utils.iso8601; + +/** + *

+ * Duration is defined as a duration of time, as specified in ISO 8601, Section 5.5.3.2. Its lexical representation is + * the ISO 8601 extended format: PnYnMnDnTnHnMnS + *

+ *
    + *
  • The "P" (period) is required
  • + *
  • "n" represents a positive number
  • + *
  • years is (Y)
  • + *
  • months is (M)
  • + *
  • days is (D)
  • + *
  • time separator is (T), required if any lower terms are given
  • + *
  • hours is (H)
  • + *
  • minutes is (M)
  • + *
  • seconds is (S)
  • + *
+ *

+ * An optional preceding minus sign ("-") is also allowed to indicate a negative duration. If the sign is omitted then a + * positive duration is assumed. For example: is a 2 hour, 5 minute, and + * 2.37 second duration + *

+ *

+ * Remark: since a duration of a day may be measured in hours may vary from 23 an 25 a duration day unit doesn't + * have a meaning, if we do not know either the start or the end, we restrict ourself to measure a duration in hours, + * minutes and seconds + *

+ * + * @author msmock + * @author gattom (reimplementation using enum) + */ +@SuppressWarnings("nls") +public class ISO8601Duration implements DurationFormat { + + /** + * The time representations available, as enum, with the associated millis. + * + * @author gattom + */ + public enum TimeDuration { + SECOND(1000, 'S'), MINUTE(60 * SECOND.duration(), 'M'), HOUR(60 * MINUTE.duration(), 'H'), DAY(24 * HOUR + .duration(), 'D'), WEEK(7 * DAY.duration(), 'W'), MONTH(30 * DAY.duration(), 'M'), YEAR(12 * MONTH + .duration(), 'Y'); + + final long millis; + final char isoChar; + + TimeDuration(long milli, char isorep) { + this.millis = milli; + this.isoChar = isorep; + } + + public long duration() { + return this.millis; + } + + public static TimeDuration getTimeDurationFor(String isostring, int unitIndex) { + char duration = isostring.charAt(unitIndex); + switch (duration) { + case 'S': + if (isostring.substring(0, unitIndex).contains("T")) { + return SECOND; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); + case 'H': + if (isostring.substring(0, unitIndex).contains("T")) { + return HOUR; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); + case 'D': + return DAY; + case 'W': + return WEEK; + case 'Y': + return YEAR; + case 'M': + if (isostring.substring(0, unitIndex).contains("T")) { + return MINUTE; + } else { + return MONTH; + } + default: + throw new NumberFormatException(duration + " is not a valid unit of time in ISO8601"); + } + } + } + + /** + * check if c is a number char including the decimal decimal dot (.) + * + * @param c + * the character to check + * @return boolean return true if the given char is a number or a decimal dot (.), false otherwise + */ + private static boolean isNumber(char c) { + + boolean isNumber = Character.isDigit(c) || (c == '.'); + return isNumber; + + } + + /** + * Parses the given string to a pseudo ISO 8601 duration + * + * @param s + * the string to be parsed to a duration which must be coded as a ISO8601 value + * @return long the time value which represents the duration + */ + @Override + public long parse(String s) { + + long newResult = 0; + + // throw exception, if the string is not of length > 2 + if (s.length() < 3) + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); + + char p = s.charAt(0); + + // the first char must be a P for ISO8601 duration + if (p != 'P') + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); + + int newposition = 1; + do { + if (s.charAt(newposition) == 'T') { + // skip the separator specifying where the time starts. + newposition++; + } + + // read the string representing the numeric value + String val = parseNumber(newposition, s); + double numVal = Double.parseDouble(val); + newposition += val.length(); + + // get the time unit + TimeDuration unit = TimeDuration.getTimeDurationFor(s, newposition); + + // skip the time duration character + newposition++; + + // increment the value. + newResult += unit.duration() * numVal; + + } while (newposition < s.length()); + + return newResult; + } + + /** + * Return the substring of s starting at index i (in s) that contains a numeric string. + * + * @param index + * The start index in string s + * @param s + * The string to analyze + * @return the substring containing the numeric portion of s starting at index i. + */ + private String parseNumber(int index, String s) { + int i = index; + int start = i; + while (i < s.length()) { + if (!isNumber(s.charAt(i))) + break; + i++; + } + String substring = s.substring(start, i); + return substring; + } + + /** + * Format the given time duration unit into the string buffer. This function displays the given duration in units of + * the given unit, and returns the remainder. + *

+ * Thus, a duration of 86401000 (one day and one second) will add the representation of one day if unit is DAY (1D) + * and return 1000 as the remainder with respect of this unit. If the given unit is HOUR, then this function adds + * 24H to the {@link StringBuilder}, and returns 1000 as the remainder. + * + * @param sb + * The {@link StringBuilder} to add the given duration with the right unit + * @param duration + * The duration to add + * @param unit + * The unit of this duration + * @return The remainder of the given duration, modulo the time unit. + */ + private long formatTimeDuration(StringBuilder sb, long duration, TimeDuration unit) { + + long remainder = duration; + if (unit.equals(TimeDuration.SECOND) || remainder >= unit.duration()) { + + long quantity = remainder / unit.duration(); + remainder = remainder % unit.duration(); + sb.append(quantity); + + if (unit.equals(TimeDuration.SECOND)) { + long millis = remainder; + if (millis == 0) { + // to not have the decimal point + } else if (millis > 99) { + sb.append("." + millis); + } else if (millis > 9) { + sb.append(".0" + millis); + } else { + sb.append(".00" + millis); + } + } + + sb.append(unit.isoChar); + } + + return remainder; + } + + /** + * Formats the given time duration to a pseudo ISO 8601 duration string + * + * @param duration + * @return String the duration formatted as a ISO8601 duration string + */ + @Override + public String format(long duration) { + + // XXX this is a preliminary help to solve the situation where this method sometimes returns P + if (duration < 0l) + throw new RuntimeException("A duration can not be negative!"); + + if (duration == 0l) + return "PT0S"; + + StringBuilder sb = new StringBuilder(); + sb.append('P'); + + long remainder = formatTimeDuration(sb, duration, TimeDuration.YEAR); + remainder = formatTimeDuration(sb, remainder, TimeDuration.MONTH); + remainder = formatTimeDuration(sb, remainder, TimeDuration.DAY); + if (remainder > 0) { + sb.append('T'); + remainder = formatTimeDuration(sb, remainder, TimeDuration.HOUR); + remainder = formatTimeDuration(sb, remainder, TimeDuration.MINUTE); + remainder = formatTimeDuration(sb, remainder, TimeDuration.SECOND); + } + return sb.toString(); + } + +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java new file mode 100644 index 000000000..1c1fed122 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java @@ -0,0 +1,72 @@ +package ch.eitchnet.utils.iso8601; + +import ch.eitchnet.utils.helper.MathHelper; + +/** + * Default factory for date formats used for serialization. + * + * @author msmock + */ +public class ISO8601FormatFactory implements FormatFactory { + + private static ISO8601FormatFactory instance = new ISO8601FormatFactory(); + + /** + * the singleton constructor + */ + private ISO8601FormatFactory() { + // singleton + } + + /** + * @return the instance + */ + public static ISO8601FormatFactory getInstance() { + return instance; + } + + @Override + public ISO8601 getDateFormat() { + return new ISO8601(); + } + + @Override + public ISO8601Duration getDurationFormat() { + return new ISO8601Duration(); + } + + @Override + public ISO8601Worktime getWorktimeFormat() { + return new ISO8601Worktime(); + } + + @Override + public ISO8601 getXmlDateFormat() { + return new ISO8601(); + } + + @Override + public ISO8601Duration getXmlDurationFormat() { + return new ISO8601Duration(); + } + + @Override + public String formatDate(long date) { + return getDateFormat().format(date); + } + + @Override + public String formatDuration(long duration) { + return getDurationFormat().format(duration); + } + + @Override + public String formatWorktime(long worktime) { + return getDurationFormat().format(worktime); + } + + @Override + public String formatFloat(double value) { + return Double.toString(MathHelper.toPrecision(value)); + } +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java new file mode 100644 index 000000000..ffd0c6c08 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ +package ch.eitchnet.utils.iso8601; + +/** + *

+ * Duration is defined as a duration of time, as specified in ISO 8601, Section 5.5.3.2. Its lexical representation is + * the ISO 8601 extended format: PnYnMnDnTnHnMnS + *

+ *
    + *
  • The "P" (period) is required
  • + *
  • "n" represents a positive number
  • + *
  • years is (Y)
  • + *
  • months is (M)
  • + *
  • days is (D)
  • + *
  • time separator is (T), required if any lower terms are given
  • + *
  • hours is (H)
  • + *
  • minutes is (M)
  • + *
  • seconds is (S)
  • + *
+ *

+ * An optional preceding minus sign ("-") is also allowed to indicate a negative duration. If the sign is omitted then a + * positive duration is assumed. For example: is a 2 hour, 5 minute, and + * 2.37 second duration + *

+ *

+ * Remark: since a duration of a day may be measured in hours may vary from 23 an 25 a duration day unit doesn't + * have a meaning, if we do not know either the start or the end, we restrict ourself to measure a duration in hours, + * minutes and seconds + *

+ * + * @author msmock + * @author gattom (reimplementation using enum) + */ +@SuppressWarnings("nls") +public class ISO8601Worktime implements WorktimeFormat { + + /** + * The time representations available, as enum, with the associated millis. + * + * @author gattom + */ + public enum TimeDuration { + + SECOND(1000, 'S'), MINUTE(60 * SECOND.duration(), 'M'), HOUR(60 * MINUTE.duration(), 'H'); + + final long millis; + final char isoChar; + + TimeDuration(long milli, char isorep) { + this.millis = milli; + this.isoChar = isorep; + } + + public long duration() { + return this.millis; + } + + public static TimeDuration getTimeDurationFor(String isostring, int unitIndex) { + char duration = isostring.charAt(unitIndex); + switch (duration) { + case 'S': + if (isostring.substring(0, unitIndex).contains("T")) { + return SECOND; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); + case 'H': + if (isostring.substring(0, unitIndex).contains("T")) { + return HOUR; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); + case 'M': + return MINUTE; + default: + throw new NumberFormatException(duration + " is not a valid unit of time in ISO8601"); + } + } + + } + + /** + * check if c is a number char including the decimal decimal dot (.) + * + * @param c + * the character to check + * @return boolean return true if the given char is a number or a decimal dot (.), false otherwise + */ + private static boolean isNumber(char c) { + boolean isNumber = Character.isDigit(c) || (c == '.'); + return isNumber; + } + + /** + * Parses the given string to a pseudo ISO 8601 duration + * + * @param s + * the string to be parsed to a duration which must be coded as a ISO8601 value + * @return long the time value which represents the duration + */ + @Override + public long parse(String s) { + + long newResult = 0; + + // throw exception, if the string is not of length > 2 + if (s.length() < 3) + throw new NumberFormatException(s + " cannot be parsed to ISA 8601 Duration"); + + char p = s.charAt(0); + + if (p == 'P') { + int newposition = 1; + do { + if (s.charAt(newposition) == 'T') { + // skip the separator specifying where the time starts. + newposition++; + } + // read the string representing the numeric value + String val = parseNumber(newposition, s); + double numVal = Double.parseDouble(val); + newposition += val.length(); + // get the time unit + TimeDuration unit = TimeDuration.getTimeDurationFor(s, newposition); + // skip the time duration character + newposition++; + // increment the value. + newResult += unit.duration() * numVal; + } while (newposition < s.length()); + + return newResult; + + } else { + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); + } + + } + + /** + * Return the substring of s starting at index i (in s) that contains a numeric string. + * + * @param index + * The start index in string s + * @param s + * The string to analyze + * @return the substring containing the numeric portion of s starting at index i. + */ + private String parseNumber(int index, String s) { + int i = index; + int start = i; + while (i < s.length()) { + if (!isNumber(s.charAt(i))) + break; + i++; + } + String substring = s.substring(start, i); + return substring; + } + + /** + * Format the given time duration unit into the string buffer. This function displays the given duration in units of + * the given unit, and returns the remainder. + *

+ * Thus, a duration of 86401000 (one day and one second) will add the representation of one day if unit is DAY (1D) + * and return 1000 as the remainder with respect of this unit. If the given unit is HOUR, then this function adds + * 24H to the {@link StringBuilder}, and returns 1000 as the remainder. + * + * @param sb + * The {@link StringBuilder} to add the given duration with the right unit + * @param duration + * The duration to add + * @param unit + * The unit of this duration + * @return The remainder of the given duration, modulo the time unit. + */ + private long formatTimeDuration(StringBuilder sb, long duration, TimeDuration unit) { + + long remainder = duration; + + if (unit.equals(TimeDuration.SECOND) || remainder >= unit.duration()) { + + long quantity = remainder / unit.duration(); + remainder = remainder % unit.duration(); + sb.append(quantity); + + if (unit.equals(TimeDuration.SECOND)) { + + long millis = remainder; + if (millis == 0) { + // to not have the decimal point + } else if (millis > 99) { + sb.append("." + millis); + } else if (millis > 9) { + sb.append(".0" + millis); + } else { + sb.append(".00" + millis); + } + } + + sb.append(unit.isoChar); + } + return remainder; + } + + /** + * Formats the given time duration to a pseudo ISO 8601 duration string + * + * @param duration + * @return String the duration formatted as a ISO8601 duration string + */ + @Override + public String format(long duration) { + + if (duration == 0) + return "PT0S"; + + StringBuilder sb = new StringBuilder(); + sb.append('P'); + sb.append('T'); + long remainder = formatTimeDuration(sb, duration, TimeDuration.HOUR); + remainder = formatTimeDuration(sb, remainder, TimeDuration.MINUTE); + remainder = formatTimeDuration(sb, remainder, TimeDuration.SECOND); + + return sb.toString(); + } + +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java new file mode 100644 index 000000000..058a5f7c3 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 + * Olten + * + * All rights reserved. + * + */ + +package ch.eitchnet.utils.iso8601; + +/** + * interface for the worktime format + * + * @author msmock + */ +public interface WorktimeFormat { + + /** + * format a long to string + * + * @param l + * @return formatted string if the long argument + */ + public String format(long l); + + /** + * parse a string to long + * + * @param s + * @return the long value parsed + */ + public long parse(String s); + +} From 459a8c096bcdd530eceaa13366f42a4cfc677d96 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 24 Oct 2013 21:53:51 +0200 Subject: [PATCH 049/180] [Minor] removed dependency to log4j - using slf4j --- src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java index 496b49e96..30be5b495 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -6,7 +6,8 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * @@ -14,7 +15,7 @@ import org.apache.log4j.Logger; @SuppressWarnings("nls") public class ISO8601 implements DateFormat { - private static final Logger logger = Logger.getLogger(ISO8601.class); + private static final Logger logger = LoggerFactory.getLogger(ISO8601.class); /** * misc. numeric formats used in formatting @@ -234,7 +235,7 @@ public class ISO8601 implements DateFormat { cal.setTime(date); return format(cal); } catch (Exception e) { - logger.error(e, e); + logger.error(e.getMessage(), e); return null; } } From ef256706e48fe5f22104b9925e32914914d825de Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 27 Oct 2013 23:06:30 +0100 Subject: [PATCH 050/180] [Minor] set parent to 0.1.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 03a84e647..143e056af 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ ch.eitchnet ch.eitchnet.parent - 0.0.1-SNAPSHOT + 0.1.0-SNAPSHOT ../ch.eitchnet.parent/pom.xml From 60e75219bb59e2d672b07e8aa927c676fec85052 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 28 Oct 2013 18:56:44 +0100 Subject: [PATCH 051/180] [Minor] cleaned up .gitignore --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index 76c4111c6..18d2ca6cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,4 @@ -*.class - -tmp/ - -# Maven target target/ - -# Eclipse settings .classpath .project .settings/ From f35ba7b13c1720a2be76fe4e4e87b9ab4e3ab35a Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 31 Oct 2013 00:06:51 +0100 Subject: [PATCH 052/180] [Minor] cleaned up XmlHelper --- .../ch/eitchnet/utils/helper/XmlHelper.java | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index aa38e70f3..d866c002a 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -21,9 +21,9 @@ package ch.eitchnet.utils.helper; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.text.MessageFormat; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -57,17 +57,17 @@ public class XmlHelper { /** * PROP_LINE_SEPARATOR = "line.separator" : the system property to fetch defined line separator */ - public static final String PROP_LINE_SEPARATOR = "line.separator"; + public static final String PROP_LINE_SEPARATOR = "line.separator"; //$NON-NLS-1$ /** * UNIX_LINE_SEP = "\n" : mostly we want this line separator, instead of the windows version */ - public static final String UNIX_LINE_SEP = "\n"; + public static final String UNIX_LINE_SEP = "\n"; //$NON-NLS-1$ /** * DEFAULT_ENCODING = "utf-8" : defines the default UTF-8 encoding expected of XML files */ - public static final String DEFAULT_ENCODING = "utf-8"; + public static final String DEFAULT_ENCODING = "utf-8"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(XmlHelper.class); @@ -79,11 +79,16 @@ public class XmlHelper { */ public static void parseDocument(File xmlFile, DefaultHandler xmlHandler) { - try { - XmlHelper.logger.info("Parsing XML document " + xmlFile.getAbsolutePath()); - parseDocument(new FileInputStream(xmlFile), xmlHandler); - } catch (FileNotFoundException e) { - throw new XmlException("The XML file could not be read: " + xmlFile.getAbsolutePath(), e); + try (FileInputStream xmlFileInputStream = new FileInputStream(xmlFile);) { + + parseDocument(xmlFileInputStream, xmlHandler); + String msg = "SAX parsed file {0}"; //$NON-NLS-1$ + logger.info(MessageFormat.format(msg, xmlFile.getAbsolutePath())); + + } catch (IOException e) { + String msg = "Failed to parse XML file: {0} due to: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, xmlFile.getAbsolutePath(), e.getMessage()); + throw new XmlException(msg, e); } } @@ -103,11 +108,11 @@ public class XmlHelper { sp.parse(xmlFileInputStream, xmlHandler); } catch (ParserConfigurationException e) { - throw new XmlException("Failed to initialize a SAX Parser: " + e.getMessage(), e); + throw new XmlException("Failed to initialize a SAX Parser: " + e.getMessage(), e); //$NON-NLS-1$ } catch (SAXException e) { - throw new XmlException("The XML stream is not parseable: " + e.getMessage(), e); + throw new XmlException("The XML stream is not parseable: " + e.getMessage(), e); //$NON-NLS-1$ } catch (IOException e) { - throw new XmlException("The XML stream not be read: " + e.getMessage(), e); + throw new XmlException("The XML stream not be read: " + e.getMessage(), e); //$NON-NLS-1$ } } @@ -124,30 +129,33 @@ public class XmlHelper { */ public static void writeDocument(Document document, File file) throws RuntimeException { - XmlHelper.logger.info("Exporting document element " + document.getNodeName() + " to " + file.getAbsolutePath()); + String msg = "Exporting document element {0} to {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, document.getNodeName(), file.getAbsolutePath()); + XmlHelper.logger.info(msg); String lineSep = System.getProperty(PROP_LINE_SEPARATOR); try { String encoding = document.getInputEncoding(); if (encoding == null || encoding.isEmpty()) { - XmlHelper.logger.info("No encoding passed. Using default encoding " + XmlHelper.DEFAULT_ENCODING); + XmlHelper.logger.info(MessageFormat.format( + "No encoding passed. Using default encoding {0}", XmlHelper.DEFAULT_ENCODING)); //$NON-NLS-1$ encoding = XmlHelper.DEFAULT_ENCODING; } - if (!lineSep.equals("\n")) { - XmlHelper.logger.info("Overriding line separator to \\n"); + if (!lineSep.equals("\n")) { //$NON-NLS-1$ + XmlHelper.logger.info("Overriding line separator to \\n"); //$NON-NLS-1$ System.setProperty(PROP_LINE_SEPARATOR, UNIX_LINE_SEP); } // Set up a transformer TransformerFactory transfac = TransformerFactory.newInstance(); Transformer transformer = transfac.newTransformer(); - transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); //$NON-NLS-1$ + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.ENCODING, encoding); - transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); + transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$ // transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", "\t"); // Transform to file @@ -157,7 +165,7 @@ public class XmlHelper { } catch (Exception e) { - throw new XmlException("Exception while exporting to file: " + e, e); + throw new XmlException("Exception while exporting to file: " + e, e); //$NON-NLS-1$ } finally { @@ -203,9 +211,9 @@ public class XmlHelper { return document; } catch (DOMException e) { - throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); + throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); //$NON-NLS-1$ } catch (ParserConfigurationException e) { - throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); + throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); //$NON-NLS-1$ } } } From 5047ad9ff0f4e2e4999ba1c93990242ca50d2ae1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 31 Oct 2013 00:41:50 +0100 Subject: [PATCH 053/180] [Major] cleaned up FileHelper by using Java 7 auto resource close - Also cleaned up all compiler warnings. - Added new method to copy a list of files to a destination --- .../ch/eitchnet/utils/helper/FileHelper.java | 175 +++++++----------- 1 file changed, 67 insertions(+), 108 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index a175dc4c5..389572504 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -32,6 +32,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -59,15 +60,13 @@ public class FileHelper { */ 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", + 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; - BufferedInputStream in = null; - try { - in = new BufferedInputStream(new FileInputStream(file)); + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));) { byte[] bytes = new byte[8192]; int read; while ((read = in.read(bytes)) != -1) { @@ -75,17 +74,9 @@ public class FileHelper { pos += read; } } catch (FileNotFoundException e) { - throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); + throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); //$NON-NLS-1$ } catch (IOException e) { - throw new RuntimeException("Could not read file " + file.getAbsolutePath()); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - FileHelper.logger.error("Failed to close InputStream: " + e.getLocalizedMessage()); - } - } + throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$ } return data; @@ -101,32 +92,22 @@ public class FileHelper { */ public static final String readFileToString(File file) { - BufferedReader bufferedReader = null; - try { + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file));) { - bufferedReader = new BufferedReader(new FileReader(file)); StringBuilder sb = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { - sb.append(line + "\n"); + sb.append(line + "\n"); //$NON-NLS-1$ } return sb.toString(); } catch (FileNotFoundException e) { - throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); + throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); //$NON-NLS-1$ } catch (IOException e) { - throw new RuntimeException("Could not read file " + file.getAbsolutePath()); - } finally { - if (bufferedReader != null) { - try { - bufferedReader.close(); - } catch (IOException e) { - FileHelper.logger.error("Failed to close BufferedReader: " + e.getLocalizedMessage()); - } - } + throw new RuntimeException("Could not read file " + file.getAbsolutePath()); //$NON-NLS-1$ } } @@ -140,24 +121,14 @@ public class FileHelper { */ public static final void writeToFile(byte[] bytes, File dstFile) { - BufferedOutputStream out = null; - try { + try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dstFile));) { - out = new BufferedOutputStream(new FileOutputStream(dstFile)); out.write(bytes); } catch (FileNotFoundException e) { - throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); + 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()); - } finally { - if (out != null) { - try { - out.close(); - } catch (IOException e) { - FileHelper.logger.error("Failed to close OutputStream: " + e.getLocalizedMessage()); - } - } + throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$ } } @@ -171,26 +142,15 @@ public class FileHelper { */ public static final void writeStringToFile(String string, File dstFile) { - BufferedWriter bufferedwriter = null; - try { + try (BufferedWriter bufferedwriter = new BufferedWriter(new FileWriter(dstFile));) { - bufferedwriter = new BufferedWriter(new FileWriter(dstFile)); bufferedwriter.write(string); - bufferedwriter.close(); } catch (FileNotFoundException e) { - throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); + 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()); - } finally { - if (bufferedwriter != null) { - try { - bufferedwriter.close(); - } catch (IOException e) { - FileHelper.logger.error("Failed to close BufferedWriter: " + e.getLocalizedMessage()); - } - } + throw new RuntimeException("Could not write to file " + dstFile.getAbsolutePath()); //$NON-NLS-1$ } } @@ -223,31 +183,60 @@ public class FileHelper { boolean done = FileHelper.deleteFiles(file.listFiles(), log); if (!done) { worked = false; - FileHelper.logger.warn("Could not empty the directory: " + file.getAbsolutePath()); + 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()); + FileHelper.logger.info("Deleted DIR " + file.getAbsolutePath()); //$NON-NLS-1$ } else { worked = false; - FileHelper.logger.warn("Could not delete the directory: " + file.getAbsolutePath()); + 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()); + FileHelper.logger.info("Deleted FILE " + file.getAbsolutePath()); //$NON-NLS-1$ } else { worked = false; - FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath())); + FileHelper.logger.warn(("Could not delete the file: " + file.getAbsolutePath())); //$NON-NLS-1$ } } } return worked; } + /** + * Copy a given list of {@link File Files}. The renameTo method does not allow action across NFS mounted filesystems + * this method is the workaround + * + * @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 renaming 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 (!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 @@ -262,12 +251,8 @@ public class FileHelper { */ public final static boolean copy(File fromFile, File toFile, boolean checksum) { - BufferedInputStream inBuffer = null; - BufferedOutputStream outBuffer = null; - try { - - inBuffer = new BufferedInputStream(new FileInputStream(fromFile)); - outBuffer = new BufferedOutputStream(new FileOutputStream(toFile)); + try (BufferedInputStream inBuffer = new BufferedInputStream(new FileInputStream(fromFile)); + BufferedOutputStream outBuffer = new BufferedOutputStream(new FileOutputStream(toFile));) { int theByte = 0; @@ -283,8 +268,8 @@ public class FileHelper { String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile)); String toFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(toFile)); if (!fromFileMD5.equals(toFileMD5)) { - FileHelper.logger.error("Copying failed, as MD5 sums are not equal: " + fromFileMD5 + " / " - + 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; @@ -293,8 +278,9 @@ public class FileHelper { // cleanup if files are not the same length if (fromFile.length() != toFile.length()) { - FileHelper.logger.error("Copying failed, as new files are not the same length: " + fromFile.length() - + " / " + toFile.length()); + String msg = MessageFormat.format("Copying failed, as new files are not the same length: {0} / {1}", //$NON-NLS-1$ + fromFile.length(), toFile.length()); + FileHelper.logger.error(msg); toFile.delete(); return false; @@ -304,24 +290,6 @@ public class FileHelper { FileHelper.logger.error(e.getMessage(), e); return false; - - } finally { - - if (inBuffer != null) { - try { - inBuffer.close(); - } catch (IOException e) { - FileHelper.logger.error("Error closing BufferedInputStream" + e); - } - } - - if (outBuffer != null) { - try { - outBuffer.close(); - } catch (IOException e) { - FileHelper.logger.error("Error closing BufferedOutputStream" + e); - } - } } return true; @@ -343,11 +311,11 @@ public class FileHelper { return true; } - FileHelper.logger.warn("Simple File.renameTo failed, trying copy/delete..."); + 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()); + FileHelper.logger.info("Deleting fromFile: " + fromFile.getAbsolutePath()); //$NON-NLS-1$ return fromFile.delete(); } @@ -473,6 +441,7 @@ public class FileHelper { * * @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); @@ -496,7 +465,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileMd5(File file) { - return FileHelper.hashFile(file, "MD5"); + return FileHelper.hashFile(file, "MD5"); //$NON-NLS-1$ } /** @@ -509,7 +478,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileSha1(File file) { - return FileHelper.hashFile(file, "SHA-1"); + return FileHelper.hashFile(file, "SHA-1"); //$NON-NLS-1$ } /** @@ -522,7 +491,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFileSha256(File file) { - return FileHelper.hashFile(file, "SHA-256"); + return FileHelper.hashFile(file, "SHA-256"); //$NON-NLS-1$ } /** @@ -537,8 +506,7 @@ public class FileHelper { * @return the hash as a byte array */ public static byte[] hashFile(File file, String algorithm) { - try { - InputStream fis = new FileInputStream(file); + try (InputStream fis = new FileInputStream(file);) { byte[] buffer = new byte[1024]; MessageDigest complete = MessageDigest.getInstance(algorithm); @@ -553,7 +521,7 @@ public class FileHelper { return complete.digest(); } catch (Exception e) { - throw new RuntimeException("Something went wrong while hashing file: " + file.getAbsolutePath()); + throw new RuntimeException("Something went wrong while hashing file: " + file.getAbsolutePath()); //$NON-NLS-1$ } } @@ -567,23 +535,14 @@ public class FileHelper { * the bytes to append */ public static void appendFilePart(File dstFile, byte[] bytes) { - FileOutputStream outputStream = null; - try { - outputStream = new FileOutputStream(dstFile, true); + 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()); - } finally { - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - FileHelper.logger.error("Exception while closing FileOutputStream " + e.getLocalizedMessage()); - } - } + throw new RuntimeException("Could not create and append the bytes to the file " + dstFile.getAbsolutePath()); //$NON-NLS-1$ } } } From 9c547af5bbdb448cea113649507a749b8457062b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 31 Oct 2013 22:42:38 +0100 Subject: [PATCH 054/180] [Major] cleaned up the FileServer/Client implentation Also renamed it from RMI to FileServer/Client. There is no reason this has to be RMI bound, as a client must simply implement FileClient where the upload/download methods can be implemented to be done over any kind of remote connection --- .../FileClient.java} | 14 +- .../FileClientUtil.java} | 116 +++++++------ .../FileDeletion.java} | 7 +- .../FileHandler.java} | 162 +++++++++++------- .../FilePart.java} | 13 +- 5 files changed, 180 insertions(+), 132 deletions(-) rename src/main/java/ch/eitchnet/{rmi/RMIFileClient.java => fileserver/FileClient.java} (84%) rename src/main/java/ch/eitchnet/{rmi/RmiHelper.java => fileserver/FileClientUtil.java} (54%) rename src/main/java/ch/eitchnet/{rmi/RmiFileDeletion.java => fileserver/FileDeletion.java} (91%) rename src/main/java/ch/eitchnet/{rmi/RmiFileHandler.java => fileserver/FileHandler.java} (53%) rename src/main/java/ch/eitchnet/{rmi/RmiFilePart.java => fileserver/FilePart.java} (90%) diff --git a/src/main/java/ch/eitchnet/rmi/RMIFileClient.java b/src/main/java/ch/eitchnet/fileserver/FileClient.java similarity index 84% rename from src/main/java/ch/eitchnet/rmi/RMIFileClient.java rename to src/main/java/ch/eitchnet/fileserver/FileClient.java index 9ddbab772..85bc54760 100644 --- a/src/main/java/ch/eitchnet/rmi/RMIFileClient.java +++ b/src/main/java/ch/eitchnet/fileserver/FileClient.java @@ -17,7 +17,7 @@ * along with ch.eitchnet.java.utils. If not, see . * */ -package ch.eitchnet.rmi; +package ch.eitchnet.fileserver; import java.rmi.RemoteException; @@ -25,7 +25,7 @@ import java.rmi.RemoteException; * @author Robert von Burg * */ -public interface RMIFileClient { +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 @@ -36,23 +36,23 @@ public interface RMIFileClient { * @throws RemoteException * if something goes wrong with the remote call */ - public void uploadFilePart(RmiFilePart filePart) throws RemoteException; + 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 RmiFileDeletion} defining the deletion request + * 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(RmiFileDeletion fileDeletion) throws RemoteException; + 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 RmiFilePart} with + * 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 @@ -65,5 +65,5 @@ public interface RMIFileClient { * @throws RemoteException * if something goes wrong with the remote call */ - public RmiFilePart requestFile(RmiFilePart filePart) throws RemoteException; + public FilePart requestFile(FilePart filePart) throws RemoteException; } diff --git a/src/main/java/ch/eitchnet/rmi/RmiHelper.java b/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java similarity index 54% rename from src/main/java/ch/eitchnet/rmi/RmiHelper.java rename to src/main/java/ch/eitchnet/fileserver/FileClientUtil.java index 049e4dbd7..934d406bc 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiHelper.java +++ b/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java @@ -17,13 +17,14 @@ * along with ch.eitchnet.java.utils. If not, see . * */ -package ch.eitchnet.rmi; +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; @@ -35,25 +36,27 @@ import ch.eitchnet.utils.helper.StringHelper; * @author Robert von Burg * */ -public class RmiHelper { +public class FileClientUtil { - private static final Logger logger = LoggerFactory.getLogger(RmiHelper.class); + private static final Logger logger = LoggerFactory.getLogger(FileClientUtil.class); /** * @param rmiFileClient * @param origFilePart * @param dstFile */ - public static void downloadFile(RMIFileClient rmiFileClient, RmiFilePart origFilePart, File 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()) - throw new RuntimeException("The destination file " + dstFile.getAbsolutePath() - + " already exists. Delete it first, if you want to overwrite it!"); + 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 { - RmiFilePart tmpPart = origFilePart; + FilePart tmpPart = origFilePart; int loops = 0; int startLength = tmpPart.getPartLength(); @@ -64,14 +67,17 @@ public class RmiHelper { tmpPart = rmiFileClient.requestFile(tmpPart); // validate length of data - if (tmpPart.getPartLength() != tmpPart.getPartBytes().length) - throw new RuntimeException("Invalid tmpPart. Part length is not as long as the bytes passed " - + tmpPart.getPartLength() + " / " + tmpPart.getPartBytes().length); + 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()) { - throw new RuntimeException("The part offset $offset is not at the end of the file " - + 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 @@ -84,28 +90,33 @@ public class RmiHelper { if (tmpPart.getPartOffset() >= tmpPart.getFileLength()) break; } - RmiHelper.logger.info(tmpPart.getFileType() + ": " + tmpPart.getFileName() + ": Requested " + loops - + " parts. StartSize: " + startLength + " EndSize: " + tmpPart.getPartLength()); + + 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()) { - throw new RuntimeException("Offset " + tmpPart.getPartOffset() + " is not at file length " - + origFilePart.getFileLength() + " after reading all the file parts!"); + 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())) { - throw new RuntimeException("Downloading the file " + origFilePart.getFileName() - + " failed because the hashes don't match. Expected: " + origFilePart.getFileHash() - + " / Actual: " + dstFileHash); + 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; - throw new RuntimeException("Downloading the file " + origFilePart.getFileName() - + " failed because of an underlying exception " + e.getLocalizedMessage()); + 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); } } @@ -114,28 +125,29 @@ public class RmiHelper { * @param srcFile * @param fileType */ - public static void uploadFile(RMIFileClient rmiFileClient, File srcFile, String fileType) { + public static void uploadFile(FileClient rmiFileClient, File srcFile, String fileType) { // make sure the source file exists - if (!srcFile.canRead()) - throw new RuntimeException("The source file does not exist at " + srcFile.getAbsolutePath()); + if (!srcFile.canRead()) { + String msg = MessageFormat.format("The source file does not exist at {0}", srcFile.getAbsolutePath()); //$NON-NLS-1$ + throw new RuntimeException(msg); + } - BufferedInputStream inputStream = null; - try { + 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 - RmiFilePart filePart = new RmiFilePart(srcFile.getName(), fileType); + 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 > RmiFileHandler.MAX_PART_SIZE) - partLength = RmiFileHandler.MAX_PART_SIZE; + if (fileLength > FileHandler.MAX_PART_SIZE) + partLength = FileHandler.MAX_PART_SIZE; else partLength = (int) fileLength; @@ -143,8 +155,6 @@ public class RmiHelper { byte[] bytes = new byte[partLength]; // open the stream to the file - inputStream = new BufferedInputStream(new FileInputStream(srcFile)); - int read = 0; int offset = 0; @@ -159,11 +169,11 @@ public class RmiHelper { // 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!"); + throw new IOException("Something went wrong while reading the bytes as -1 was returned!"); //$NON-NLS-1$ if (read != bytes.length) { - throw new IOException( - "Something went wrong while reading the bytes as the wrong number of bytes were read. Expected " - + bytes.length + " Actual: " + read); + 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 @@ -187,9 +197,11 @@ public class RmiHelper { // the last part of the file if (nextOffset + bytes.length > fileLength) { long remaining = fileLength - nextOffset; - if (remaining > RmiFileHandler.MAX_PART_SIZE) - throw new RuntimeException("Something went wrong as the remaining part " + remaining - + " is larger than MAX_PART_SIZE " + RmiFileHandler.MAX_PART_SIZE + "!"); + 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]; } @@ -198,22 +210,17 @@ public class RmiHelper { offset = nextOffset; } - RmiHelper.logger.info(filePart.getFileType() + ": " + filePart.getFileName() + ": Sent " + loops - + " parts. StartSize: " + startLength + " EndSize: " + filePart.getPartLength()); + 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; - throw new RuntimeException("Uploading the file " + srcFile.getAbsolutePath() - + " failed because of an underlying exception " + e.getLocalizedMessage()); - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - RmiHelper.logger.error("Exception while closing FileInputStream " + e.getLocalizedMessage()); - } - } + 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); } } @@ -222,13 +229,14 @@ public class RmiHelper { * @param fileDeletion * @param dstFile */ - public static void deleteFile(RMIFileClient rmiFileClient, RmiFileDeletion fileDeletion, File dstFile) { + public static void deleteFile(FileClient rmiFileClient, FileDeletion fileDeletion, File dstFile) { try { rmiFileClient.deleteFile(fileDeletion); } catch (RemoteException e) { - throw new RuntimeException("Deleting the file " + fileDeletion.getFileName() - + " failed because of an underlying exception " + e.getLocalizedMessage()); + 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/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java b/src/main/java/ch/eitchnet/fileserver/FileDeletion.java similarity index 91% rename from src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java rename to src/main/java/ch/eitchnet/fileserver/FileDeletion.java index 3071092f0..eecbb7050 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileDeletion.java +++ b/src/main/java/ch/eitchnet/fileserver/FileDeletion.java @@ -17,16 +17,15 @@ * along with ch.eitchnet.java.utils. If not, see . * */ -package ch.eitchnet.rmi; +package ch.eitchnet.fileserver; import java.io.Serializable; /** * @author Robert von Burg */ -public class RmiFileDeletion implements Serializable { +public class FileDeletion implements Serializable { - // private static final long serialVersionUID = 1L; private String fileName; @@ -38,7 +37,7 @@ public class RmiFileDeletion implements Serializable { * @param fileType * the type of file to delete. This defines in which path the file resides */ - public RmiFileDeletion(String fileName, String fileType) { + public FileDeletion(String fileName, String fileType) { this.fileName = fileName; this.fileType = fileType; } diff --git a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java b/src/main/java/ch/eitchnet/fileserver/FileHandler.java similarity index 53% rename from src/main/java/ch/eitchnet/rmi/RmiFileHandler.java rename to src/main/java/ch/eitchnet/fileserver/FileHandler.java index 6dcc256b4..7267c562f 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFileHandler.java +++ b/src/main/java/ch/eitchnet/fileserver/FileHandler.java @@ -17,12 +17,13 @@ * along with ch.eitchnet.java.utils. If not, see . * */ -package ch.eitchnet.rmi; +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; @@ -32,13 +33,13 @@ 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(RmiFilePart)} and the downloading a file is done by calling {@link #requestFile(RmiFilePart)} + * {@link #handleFilePart(FilePart)} and the downloading a file is done by calling {@link #requestFile(FilePart)} * * @author Robert von Burg */ -public class RmiFileHandler { +public class FileHandler { - private static final Logger logger = LoggerFactory.getLogger(RmiFileHandler.class); + private static final Logger logger = LoggerFactory.getLogger(FileHandler.class); /** * DEF_PART_SIZE = default part size which is set to 1048576 bytes (1 MiB) @@ -46,23 +47,29 @@ public class RmiFileHandler { public static final int MAX_PART_SIZE = 1048576; private String basePath; + private boolean verbose; /** * */ - public RmiFileHandler(String basePath) { + public FileHandler(String basePath, boolean verbose) { File basePathF = new File(basePath); - if (!basePathF.exists()) - throw new RuntimeException("Base Path does not exist " + basePathF.getAbsolutePath()); - if (!basePathF.canWrite()) - throw new RuntimeException("Can not write to base path " + basePathF.getAbsolutePath()); + 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 RmiFilePart} with a byte + * 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 @@ -70,7 +77,7 @@ public class RmiFileHandler { * @param filePart * the part of the file */ - public RmiFilePart requestFile(RmiFilePart filePart) { + public FilePart requestFile(FilePart filePart) { // validate file name is legal String fileName = filePart.getFileName(); @@ -81,12 +88,15 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File file = new File(this.basePath + "/" + fileType, filePart.getFileName()); + 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()) { - throw new RuntimeException("The file " + fileName - + " could not be found in the location for files of type " + fileType); + 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 @@ -103,18 +113,21 @@ public class RmiFileHandler { // variables defining the part of the file we're going to return long requestOffset = filePart.getPartOffset(); int requestSize = filePart.getPartLength(); - if (requestSize > RmiFileHandler.MAX_PART_SIZE) { - throw new RuntimeException("The requested part size " + requestSize + " is greater than the allowed " - + RmiFileHandler.MAX_PART_SIZE); + 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) { - throw new RuntimeException("The part request has a file size " + filePart.getFileLength() - + ", but the file is actually " + 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) { - throw new RuntimeException("The requested file part offset " + requestOffset - + " is greater than the size of the file " + 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 @@ -126,8 +139,11 @@ public class RmiFileHandler { long l = Math.min(requestSize, remaining); // this is a fail safe - if (l > RmiFileHandler.MAX_PART_SIZE) - throw new RuntimeException("Something went wrong. Min of requestSize and remaining is > MAX_PART_SIZE!"); + 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; @@ -136,40 +152,42 @@ public class RmiFileHandler { } // now read the part of the file and set it as bytes for the file part - FileInputStream fin = null; - try { + try (FileInputStream fin = new FileInputStream(file);) { // position the stream - fin = new FileInputStream(file); long skip = fin.skip(requestOffset); - if (skip != requestOffset) - throw new IOException("Asked to skip " + requestOffset + " but only skipped " + skip); + 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) - throw new IOException("Asked to read " + requestSize + " but only read " + read); + 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) { - throw new RuntimeException("The file " + fileName - + " could not be found in the location for files of type " + fileType); + String msg = MessageFormat.format(fileNotFoundMsg, fileName, fileType); + throw new RuntimeException(msg); } catch (IOException e) { - throw new RuntimeException("There was an error while reading from the file " + fileName); - } finally { - if (fin != null) { - try { - fin.close(); - } catch (IOException e) { - RmiFileHandler.logger.error("Error while closing FileInputStream: " + e.getLocalizedMessage()); - } - } + 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 edited + // 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; } @@ -180,7 +198,7 @@ public class RmiFileHandler { * @param filePart * the part of the file */ - public void handleFilePart(RmiFilePart filePart) { + public void handleFilePart(FilePart filePart) { // validate file name is legal String fileName = filePart.getFileName(); @@ -191,11 +209,14 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File dstFile = new File(this.basePath + "/" + fileType, filePart.getFileName()); + 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()) { - throw new RuntimeException("The file " + fileName + " already exist for type " + fileType); + 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 @@ -205,23 +226,34 @@ public class RmiFileHandler { if (filePart.isLastPart()) { String dstFileHash = StringHelper.getHexString(FileHelper.hashFileSha256(dstFile)); if (!dstFileHash.equals(filePart.getFileHash())) { - throw new RuntimeException("Uploading the file " + filePart.getFileName() - + " failed because the hashes don't match. Expected: " + filePart.getFileHash() + " / Actual: " - + dstFileHash); + 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 RmiFileDeletion} defining the deletion request + * the {@link FileDeletion} defining the deletion request * * @return true if the file was deleted, false if the file did not exist * */ - public boolean deleteFile(RmiFileDeletion fileDeletion) { + public boolean deleteFile(FileDeletion fileDeletion) { // validate file name is legal String fileName = fileDeletion.getFileName(); @@ -232,10 +264,20 @@ public class RmiFileHandler { validateFileType(fileType); // evaluate the path where the file should reside - File fileToDelete = new File(this.basePath + "/" + fileType, fileDeletion.getFileName()); + String fileTypePath = this.basePath + "/" + fileType; //$NON-NLS-1$ + File fileToDelete = new File(fileTypePath, fileDeletion.getFileName()); // delete the file - return FileHelper.deleteFiles(new File[] { fileToDelete }, true); + 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; } /** @@ -246,10 +288,10 @@ public class RmiFileHandler { 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!"); - } else if (fileName.contains("/")) { - throw new RuntimeException( - "The given file name contains illegal characters. The file name may not contain slashes!"); + 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); } } @@ -260,10 +302,10 @@ public class RmiFileHandler { */ 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!"); - } else if (fileType.contains("/")) { - throw new RuntimeException( - "The given file type contains illegal characters. The file type may not contain slashes!"); + 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/src/main/java/ch/eitchnet/rmi/RmiFilePart.java b/src/main/java/ch/eitchnet/fileserver/FilePart.java similarity index 90% rename from src/main/java/ch/eitchnet/rmi/RmiFilePart.java rename to src/main/java/ch/eitchnet/fileserver/FilePart.java index 627fcc373..dfdc19615 100644 --- a/src/main/java/ch/eitchnet/rmi/RmiFilePart.java +++ b/src/main/java/ch/eitchnet/fileserver/FilePart.java @@ -17,16 +17,15 @@ * along with ch.eitchnet.java.utils. If not, see . * */ -package ch.eitchnet.rmi; +package ch.eitchnet.fileserver; import java.io.Serializable; /** * @author Robert von Burg */ -public class RmiFilePart implements Serializable { +public class FilePart implements Serializable { - // private static final long serialVersionUID = 1L; private String fileName; @@ -45,18 +44,18 @@ public class RmiFilePart implements Serializable { * @param fileType * defines the type of file being uploaded or retrieved. This defines in which path the file resides */ - public RmiFilePart(String fileName, String fileType) { + public FilePart(String fileName, String fileType) { if (fileName == null || fileName.isEmpty()) - throw new RuntimeException("fileName may not be empty!"); + throw new RuntimeException("fileName may not be empty!"); //$NON-NLS-1$ if (fileType == null || fileType.isEmpty()) - throw new RuntimeException("fileType may not be empty!"); + throw new RuntimeException("fileType may not be empty!"); //$NON-NLS-1$ this.fileName = fileName; this.fileType = fileType; this.partOffset = 0; - this.partLength = RmiFileHandler.MAX_PART_SIZE; + this.partLength = FileHandler.MAX_PART_SIZE; this.partBytes = null; } From 994de0241ad7c64e08824b676f385c5a75b29477 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 1 Nov 2013 18:48:57 +0100 Subject: [PATCH 055/180] [Minor] cleaned up compiler warnings in BaseEncoding --- .../eitchnet/utils/helper/BaseEncoding.java | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 49d5f0e9e..7a0513c43 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -21,6 +21,8 @@ */ package ch.eitchnet.utils.helper; +import java.text.MessageFormat; + /** *

* This class implements the encoding and decoding of RFC 4648 https://tools.ietf.org/html/rfc4648. @@ -368,8 +370,10 @@ public class BaseEncoding { public static byte[] toBase64(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) return new byte[0]; - if (alphabet.length != 64) - throw new RuntimeException("Alphabet does not have expected size 64 but is " + alphabet.length); + 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 @@ -476,8 +480,10 @@ public class BaseEncoding { public static byte[] toBase32(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) return new byte[0]; - if (alphabet.length != 32) - throw new RuntimeException("Alphabet does not have expected size 32 but is " + alphabet.length); + 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 @@ -598,8 +604,10 @@ public class BaseEncoding { public static byte[] toBase16(byte[] alphabet, byte[] bytes) { if (bytes.length == 0) return new byte[0]; - if (alphabet.length != 16) - throw new RuntimeException("Alphabet does not have expected size 16 but is " + alphabet.length); + 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; @@ -651,15 +659,19 @@ public class BaseEncoding { if (inputLength == 0) return new byte[0]; if ((inputLength % 4) != 0) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " - + (inputLength % 4)); + 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) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + 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!"); + 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; @@ -786,15 +798,18 @@ public class BaseEncoding { if (inputLength == 0) return new byte[0]; if ((inputLength % 8) != 0) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 8, but is multiple of " - + (inputLength % 8)); + 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) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + 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!"); + 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; @@ -945,15 +960,18 @@ public class BaseEncoding { if (bytes.length == 0) return new byte[0]; if ((bytes.length % 2) != 0) { - throw new RuntimeException("The input bytes to be decoded must be multiples of 4, but is multiple of " - + (bytes.length % 4)); + 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) - throw new RuntimeException("Alphabet does not have expected size 128 but is " + alphabet.length); + 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!"); + throw new RuntimeException("The data contains illegal values which are not mapped by the given alphabet!"); //$NON-NLS-1$ int dataLength = bytes.length / 2; @@ -963,25 +981,27 @@ public class BaseEncoding { 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) { - throw new IllegalArgumentException("Value at index " + (i - 2) + " is not in range of alphabet (0-127)" - + b1); + msgOutOfRange = MessageFormat.format(msgOutOfRange, (i - 2), b1); + throw new IllegalArgumentException(msgOutOfRange); } if (b2 < 0) { - throw new IllegalArgumentException("Value at index " + (i - 1) + " is not in range of alphabet (0-127)" - + b2); + 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) { - throw new IllegalArgumentException("Value at index " + (i - 2) - + " is referencing illegal value in alphabet: " + b1); + msgIllegalValue = MessageFormat.format(msgIllegalValue, (i - 2), b1); + throw new IllegalArgumentException(msgIllegalValue); } if (c2 == -1) { - throw new IllegalArgumentException("Value at index " + (i - 2) - + " is referencing illegal value in alphabet: " + b2); + msgIllegalValue = MessageFormat.format(msgIllegalValue, (i - 2), b2); + throw new IllegalArgumentException(msgIllegalValue); } int dataIndex = (i / 2) - 1; From 03663ea8d330f0cca1610f45973a266c7190777d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 18 Nov 2013 19:18:59 +0100 Subject: [PATCH 056/180] [New] Refactoring code for ISO8601 formatting/parsing and added Date --- .../ch/eitchnet/utils/iso8601/DateFormat.java | 23 +++++++++-- .../eitchnet/utils/iso8601/FormatFactory.java | 4 +- .../ch/eitchnet/utils/iso8601/ISO8601.java | 41 +++++++++++++------ .../utils/iso8601/ISO8601FormatFactory.java | 4 +- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java index d7d918dea..45022a48b 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java @@ -8,6 +8,8 @@ package ch.eitchnet.utils.iso8601; +import java.util.Date; + /** * interface for all date formats internally used by rsp applications * @@ -18,10 +20,18 @@ public interface DateFormat { /** * format a long to string * - * @param l + * @param timepoint * @return the formatted string of the long value */ - public String format(long l); + public String format(long timepoint); + + /** + * format a Date to string + * + * @param date + * @return the formatted string of the long value + */ + public String format(Date date); /** * parse a string to long @@ -29,6 +39,13 @@ public interface DateFormat { * @param s * @return the value parsed */ - public long parse(String s); + public long parseLong(String s); + /** + * parse a string to Date + * + * @param s + * @return the value parsed + */ + public Date parse(String s); } diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java index 99d43ee76..e92175a02 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java @@ -1,5 +1,7 @@ package ch.eitchnet.utils.iso8601; +import java.util.Date; + /** * This interface defines methods for formatting values for UI representation and also defines factory methods for * formatters for parsing and formatting duration and date values @@ -51,7 +53,7 @@ public interface FormatFactory { * * @return String representation of the date */ - public String formatDate(long date); + public String formatDate(Date date); /** * Formats a duration using {@link #getDateFormat()} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java index 30be5b495..4788a9de0 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -9,6 +9,8 @@ import java.util.TimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ch.eitchnet.utils.helper.StringHelper; + /** * */ @@ -17,9 +19,7 @@ public class ISO8601 implements DateFormat { private static final Logger logger = LoggerFactory.getLogger(ISO8601.class); - /** - * misc. numeric formats used in formatting - */ + //misc. numeric formats used in formatting private DecimalFormat xxFormat = new DecimalFormat("00"); private DecimalFormat xxxFormat = new DecimalFormat("000"); private DecimalFormat xxxxFormat = new DecimalFormat("0000"); @@ -29,10 +29,6 @@ public class ISO8601 implements DateFormat { */ private Calendar parseToCalendar(String text) { - if (text == null) { - throw new IllegalArgumentException("argument can not be null"); - } - // check optional leading sign char sign; int start; @@ -214,6 +210,13 @@ public class ISO8601 implements DateFormat { return sWriter.toString(); } + @Override + public String format(Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + return format(cal); + } + /** * added by msmock convert a long to ISO8601 * @@ -240,6 +243,11 @@ public class ISO8601 implements DateFormat { } } + @Override + public long parseLong(String s) { + return parse(s).getTime(); + } + /** * parse ISO8601 date to long * @@ -249,17 +257,26 @@ public class ISO8601 implements DateFormat { * @throws NumberFormatException */ @Override - public long parse(String s) { + public Date parse(String s) { - if (s.equals("-")) - return Long.MAX_VALUE; + if (StringHelper.isEmpty(s)) { + String msg = "An empty value can not pe parsed to a date!"; + throw new IllegalArgumentException(msg); + } + + if (s.equals("-")) { + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("GMT0")); + return cal.getTime(); + } Calendar cal = parseToCalendar(s); if (cal != null) { - return cal.getTime().getTime(); + return cal.getTime(); } String msg = "Input string " + s + " cannot be parsed to date."; - throw new NumberFormatException(msg); + throw new IllegalArgumentException(msg); } } diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java index 1c1fed122..93775a8b8 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java @@ -1,5 +1,7 @@ package ch.eitchnet.utils.iso8601; +import java.util.Date; + import ch.eitchnet.utils.helper.MathHelper; /** @@ -51,7 +53,7 @@ public class ISO8601FormatFactory implements FormatFactory { } @Override - public String formatDate(long date) { + public String formatDate(Date date) { return getDateFormat().format(date); } From 7ad38c1916836d76f6d26a5a576eb07baf8dfa63 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 15 Dec 2013 13:38:33 +0100 Subject: [PATCH 057/180] [Project] Changed all licence references to Apache License 2.0 --- COPYING | 674 ------------------ COPYING.LESSER | 165 ----- LICENSE | 202 ++++++ .../ch/eitchnet/fileserver/FileClient.java | 26 +- .../eitchnet/fileserver/FileClientUtil.java | 26 +- .../ch/eitchnet/fileserver/FileDeletion.java | 26 +- .../ch/eitchnet/fileserver/FileHandler.java | 26 +- .../java/ch/eitchnet/fileserver/FilePart.java | 26 +- .../utils/exceptions/XmlException.java | 32 +- .../eitchnet/utils/helper/ArraysHelper.java | 32 +- .../eitchnet/utils/helper/BaseEncoding.java | 32 +- .../ch/eitchnet/utils/helper/ByteHelper.java | 32 +- .../ch/eitchnet/utils/helper/ClassHelper.java | 15 + .../ch/eitchnet/utils/helper/FileHelper.java | 26 +- .../ch/eitchnet/utils/helper/MathHelper.java | 19 +- .../eitchnet/utils/helper/ProcessHelper.java | 26 +- .../utils/helper/PropertiesHelper.java | 32 +- .../eitchnet/utils/helper/StringHelper.java | 26 +- .../eitchnet/utils/helper/SystemHelper.java | 26 +- .../ch/eitchnet/utils/helper/XmlHelper.java | 26 +- .../ch/eitchnet/utils/iso8601/DateFormat.java | 15 +- .../utils/iso8601/DurationFormat.java | 15 +- .../eitchnet/utils/iso8601/FormatFactory.java | 15 + .../ch/eitchnet/utils/iso8601/ISO8601.java | 15 + .../utils/iso8601/ISO8601Duration.java | 14 +- .../utils/iso8601/ISO8601FormatFactory.java | 15 + .../utils/iso8601/ISO8601Worktime.java | 14 +- .../utils/iso8601/WorktimeFormat.java | 15 +- .../utils/objectfilter/ObjectCache.java | 26 +- .../utils/objectfilter/ObjectFilter.java | 26 +- .../utils/objectfilter/Operation.java | 26 +- .../utils/helper/BaseDecodingTest.java | 32 +- .../utils/helper/BaseEncodingTest.java | 32 +- .../GenerateReverseBaseEncodingAlphabets.java | 32 +- .../utils/objectfilter/ObjectFilterTest.java | 28 +- 35 files changed, 587 insertions(+), 1228 deletions(-) delete mode 100644 COPYING delete mode 100644 COPYING.LESSER create mode 100644 LICENSE diff --git a/COPYING b/COPYING deleted file mode 100644 index 94a9ed024..000000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program 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. - - This program 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 this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/COPYING.LESSER b/COPYING.LESSER deleted file mode 100644 index 65c5ca88a..000000000 --- a/COPYING.LESSER +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/fileserver/FileClient.java b/src/main/java/ch/eitchnet/fileserver/FileClient.java index 85bc54760..3fc356054 100644 --- a/src/main/java/ch/eitchnet/fileserver/FileClient.java +++ b/src/main/java/ch/eitchnet/fileserver/FileClient.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java b/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java index 934d406bc..102f205d6 100644 --- a/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java +++ b/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/fileserver/FileDeletion.java b/src/main/java/ch/eitchnet/fileserver/FileDeletion.java index eecbb7050..b669aa39e 100644 --- a/src/main/java/ch/eitchnet/fileserver/FileDeletion.java +++ b/src/main/java/ch/eitchnet/fileserver/FileDeletion.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/fileserver/FileHandler.java b/src/main/java/ch/eitchnet/fileserver/FileHandler.java index 7267c562f..bee06ab65 100644 --- a/src/main/java/ch/eitchnet/fileserver/FileHandler.java +++ b/src/main/java/ch/eitchnet/fileserver/FileHandler.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/fileserver/FilePart.java b/src/main/java/ch/eitchnet/fileserver/FilePart.java index dfdc19615..cfd2f5ce3 100644 --- a/src/main/java/ch/eitchnet/fileserver/FilePart.java +++ b/src/main/java/ch/eitchnet/fileserver/FilePart.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java index 40dec4201..1071eeee2 100644 --- a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java +++ b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java @@ -1,23 +1,17 @@ /* - * Copyright (c) 2012, Robert von Burg - * - * All rights reserved. - * - * This file is part of the ch.eitchnet.utils. - * - * ch.eitchnet.utils 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. - * - * ch.eitchnet.utils 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 ch.eitchnet.utils. If not, see - * . + * 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.exceptions; diff --git a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java index 74a22a8cf..fdb837ec4 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java @@ -1,23 +1,17 @@ /* - * 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 - * . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java index 7a0513c43..8a3437a49 100644 --- a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java +++ b/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java @@ -1,23 +1,17 @@ /* - * Copyright (c) 2012, Robert von Burg - * - * All rights reserved. - * - * This file is part of the ch.eitchnet.java.utils. - * - * ch.eitchnet.java.utils 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. - * - * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see - * . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java index 9fc47cded..b9d4170c3 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java @@ -1,23 +1,17 @@ /* - * Copyright (c) 2012, Robert von Burg - * - * All rights reserved. - * - * This file is part of the ch.eitchnet.java.utils. - * - * ch.eitchnet.java.utils 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. - * - * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see - * . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java index bcd1d496d..c25fece89 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 389572504..5db00ea85 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java index 6f46850b2..2b4b1f34b 100644 --- a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java @@ -1,15 +1,18 @@ /* - * Created on 05.01.2004 - */ - -/* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index b2c974b48..6c8b908f0 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java index 1c4fcf31a..4e0056492 100644 --- a/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java @@ -1,23 +1,17 @@ /* - * 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 - * . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index df8f094f4..b322d57cc 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index 57dad260d..f0fa37734 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index d866c002a..bf618b412 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2010 - 2012 + * Copyright 2013 Robert von Burg * - * This file is part of ch.eitchnet.java.utils. - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java index 45022a48b..edbb7f646 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java @@ -1,11 +1,18 @@ /* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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.iso8601; import java.util.Date; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java index d7d7c6f00..411c70d9c 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java @@ -1,11 +1,18 @@ /* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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.iso8601; /** diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java index e92175a02..931d1c720 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java @@ -1,3 +1,18 @@ +/* + * 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.iso8601; import java.util.Date; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java index 4788a9de0..2c9f1de68 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -1,3 +1,18 @@ +/* + * 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.iso8601; import java.text.DecimalFormat; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java index 810b7b34d..c319c719e 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -1,9 +1,17 @@ /* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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.iso8601; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java index 93775a8b8..682ad6bbd 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java @@ -1,3 +1,18 @@ +/* + * 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.iso8601; import java.util.Date; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java index ffd0c6c08..81f96cfa5 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java @@ -1,9 +1,17 @@ /* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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.iso8601; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java index 058a5f7c3..48fbf65b3 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java @@ -1,11 +1,18 @@ /* - * Copyright (c) 2006 - 2011 Apixxo AG Hauptgasse 25 4600 - * Olten + * Copyright 2013 Martin Smock * - * All rights reserved. + * 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.iso8601; /** diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index 382baffc8..1a9bf9fde 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Michael Gatto * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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.objectfilter; diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index cc0f9123c..b2282e9ca 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Michael Gatto * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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.objectfilter; diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java b/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java index 1dce6362a..a54197fe4 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 + * Copyright 2013 Michael Gatto * - * This file is part of ch.eitchnet.java.utils - * - * ch.eitchnet.java.utils is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ch.eitchnet.java.utils is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ch.eitchnet.java.utils. If not, see . + * 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.objectfilter; diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java index fee6c668e..7444a0ef6 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -1,23 +1,17 @@ /* - * 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 - * . + * 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; diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index 7bca92e75..15a62eb32 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -1,23 +1,17 @@ /* - * Copyright (c) 2012, Robert von Burg - * - * All rights reserved. - * - * This file is part of the ch.eitchnet.java.utils. - * - * ch.eitchnet.java.utils 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. - * - * ch.eitchnet.java.utils 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 ch.eitchnet.java.utils. If not, see - * . + * 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; diff --git a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java index 3e22f4331..562368b75 100644 --- a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java +++ b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java @@ -1,23 +1,17 @@ /* - * 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 - * . + * 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; diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java index ea0b7e6f9..f7a7c99df 100644 --- a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -1,21 +1,17 @@ /* - * Copyright (c) 2012 - * - * This file is part of ??????????????? - * - * ?????????????? is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Privilege is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ????????????????. If not, see . + * 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.objectfilter; From a43be2cb8868a549c48c0a7f1292133140f68131 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 18 Dec 2013 17:46:04 +0100 Subject: [PATCH 058/180] [New] Added DBC class for helping in assertions --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/dbc/DBC.java diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java new file mode 100644 index 000000000..3ea0bb26e --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -0,0 +1,56 @@ +/* + * 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.dbc; + +import java.text.MessageFormat; + +import ch.eitchnet.utils.helper.StringHelper; + +/** + * @author Robert von Burg + */ +public enum DBC { + + PRE { + public void assertNotEmpty(String msg, String value) { + if (StringHelper.isEmpty(value)) { + String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + @Override + public void assertNotNull(String msg, Object value) { + if (value == null) { + String ex = "Illegal null value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + }; + + public abstract void assertNotEmpty(String msg, String value); + public abstract void assertNotNull(String msg, Object value); + + public class DbcException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public DbcException(String message) { + super(message); + } + } +} From da835fcb7828af30c1f05d8e69937e7b90c2ee08 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 18 Dec 2013 17:46:26 +0100 Subject: [PATCH 059/180] [New] added FileHelper.readStreamToString() --- .../ch/eitchnet/utils/helper/FileHelper.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 5db00ea85..862598d9b 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -27,6 +27,7 @@ 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; @@ -101,12 +102,40 @@ public class FileHelper { return sb.toString(); } catch (FileNotFoundException e) { - throw new RuntimeException("Filed does not exist " + file.getAbsolutePath()); //$NON-NLS-1$ + 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 * From ae5bba3b4004d9ded08dbff24bbacf13d9fb5dbe Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 22 Dec 2013 23:05:06 +0100 Subject: [PATCH 060/180] [New] Added DomUtil --- .../ch/eitchnet/utils/helper/DomUtil.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/DomUtil.java diff --git a/src/main/java/ch/eitchnet/utils/helper/DomUtil.java b/src/main/java/ch/eitchnet/utils/helper/DomUtil.java new file mode 100644 index 000000000..20851de57 --- /dev/null +++ b/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); + } + } +} From ff52be1c071a87591c18f1df55e4454c93b9da5f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 24 Dec 2013 02:42:33 +0100 Subject: [PATCH 061/180] [Minor] removed use of deprecated JUnit classes --- .../utils/helper/BaseDecodingTest.java | 77 +++++++++---------- .../utils/helper/BaseEncodingTest.java | 76 +++++++++--------- 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java index 7444a0ef6..a77e5ee5c 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java @@ -21,17 +21,16 @@ import static ch.eitchnet.utils.helper.BaseEncoding.fromBase32Dmedia; import static ch.eitchnet.utils.helper.BaseEncoding.fromBase32Hex; import static ch.eitchnet.utils.helper.BaseEncoding.fromBase64; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; -import junit.framework.Assert; +import static org.junit.Assert.assertEquals; import org.junit.Test; -import org.junit.runners.JUnit4; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Robert von Burg - * */ +@SuppressWarnings("nls") public class BaseDecodingTest { public static final String PROP_RUN_PERF_TESTS = "ch.eitchnet.utils.test.runPerfTests"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(BaseDecodingTest.class); @@ -45,59 +44,59 @@ public class BaseDecodingTest { @Test public void testBase64() { - Assert.assertEquals("", fromBase64("")); - Assert.assertEquals("f", fromBase64("Zg==")); - Assert.assertEquals("fo", fromBase64("Zm8=")); - Assert.assertEquals("foo", fromBase64("Zm9v")); - Assert.assertEquals("foob", fromBase64("Zm9vYg==")); - Assert.assertEquals("fooba", fromBase64("Zm9vYmE=")); - Assert.assertEquals("foobar", fromBase64("Zm9vYmFy")); + assertEquals("", fromBase64("")); + assertEquals("f", fromBase64("Zg==")); + assertEquals("fo", fromBase64("Zm8=")); + assertEquals("foo", fromBase64("Zm9v")); + assertEquals("foob", fromBase64("Zm9vYg==")); + assertEquals("fooba", fromBase64("Zm9vYmE=")); + assertEquals("foobar", fromBase64("Zm9vYmFy")); } @Test public void testBase32() { - Assert.assertEquals("", fromBase32("")); - Assert.assertEquals("f", fromBase32("MY======")); - Assert.assertEquals("fo", fromBase32("MZXQ====")); - Assert.assertEquals("foo", fromBase32("MZXW6===")); - Assert.assertEquals("foob", fromBase32("MZXW6YQ=")); - Assert.assertEquals("fooba", fromBase32("MZXW6YTB")); - Assert.assertEquals("foobar", fromBase32("MZXW6YTBOI======")); + assertEquals("", fromBase32("")); + assertEquals("f", fromBase32("MY======")); + assertEquals("fo", fromBase32("MZXQ====")); + assertEquals("foo", fromBase32("MZXW6===")); + assertEquals("foob", fromBase32("MZXW6YQ=")); + assertEquals("fooba", fromBase32("MZXW6YTB")); + assertEquals("foobar", fromBase32("MZXW6YTBOI======")); } @Test public void testBase32Hex() { - Assert.assertEquals("", fromBase32Hex("")); - Assert.assertEquals("f", fromBase32Hex("CO======")); - Assert.assertEquals("fo", fromBase32Hex("CPNG====")); - Assert.assertEquals("foo", fromBase32Hex("CPNMU===")); - Assert.assertEquals("foob", fromBase32Hex("CPNMUOG=")); - Assert.assertEquals("fooba", fromBase32Hex("CPNMUOJ1")); - Assert.assertEquals("foobar", fromBase32Hex("CPNMUOJ1E8======")); + assertEquals("", fromBase32Hex("")); + assertEquals("f", fromBase32Hex("CO======")); + assertEquals("fo", fromBase32Hex("CPNG====")); + assertEquals("foo", fromBase32Hex("CPNMU===")); + assertEquals("foob", fromBase32Hex("CPNMUOG=")); + assertEquals("fooba", fromBase32Hex("CPNMUOJ1")); + assertEquals("foobar", fromBase32Hex("CPNMUOJ1E8======")); } @Test public void testBase32Dmedia() { - Assert.assertEquals("", fromBase32Dmedia("")); - Assert.assertEquals("binary foo", fromBase32Dmedia("FCNPVRELI7J9FUUI")); - Assert.assertEquals("f", fromBase32Dmedia("FR======")); - Assert.assertEquals("fo", fromBase32Dmedia("FSQJ====")); - Assert.assertEquals("foo", fromBase32Dmedia("FSQPX===")); - Assert.assertEquals("foob", fromBase32Dmedia("FSQPXRJ=")); - Assert.assertEquals("fooba", fromBase32Dmedia("FSQPXRM4")); - Assert.assertEquals("foobar", fromBase32Dmedia("FSQPXRM4HB======")); + assertEquals("", fromBase32Dmedia("")); + assertEquals("binary foo", fromBase32Dmedia("FCNPVRELI7J9FUUI")); + assertEquals("f", fromBase32Dmedia("FR======")); + assertEquals("fo", fromBase32Dmedia("FSQJ====")); + assertEquals("foo", fromBase32Dmedia("FSQPX===")); + assertEquals("foob", fromBase32Dmedia("FSQPXRJ=")); + assertEquals("fooba", fromBase32Dmedia("FSQPXRM4")); + assertEquals("foobar", fromBase32Dmedia("FSQPXRM4HB======")); } @Test public void testBase16() { - Assert.assertEquals("", fromBase16("")); - Assert.assertEquals("f", fromBase16("66")); - Assert.assertEquals("fo", fromBase16("666F")); - Assert.assertEquals("foo", fromBase16("666F6F")); - Assert.assertEquals("foob", fromBase16("666F6F62")); - Assert.assertEquals("fooba", fromBase16("666F6F6261")); - Assert.assertEquals("foobar", fromBase16("666F6F626172")); + assertEquals("", fromBase16("")); + assertEquals("f", fromBase16("66")); + assertEquals("fo", fromBase16("666F")); + assertEquals("foo", fromBase16("666F6F")); + assertEquals("foob", fromBase16("666F6F62")); + assertEquals("fooba", fromBase16("666F6F6261")); + assertEquals("foobar", fromBase16("666F6F626172")); } @Test diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java index 15a62eb32..4c2da50d7 100644 --- a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java @@ -22,7 +22,7 @@ import static ch.eitchnet.utils.helper.BaseEncoding.toBase32; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Dmedia; import static ch.eitchnet.utils.helper.BaseEncoding.toBase32Hex; import static ch.eitchnet.utils.helper.BaseEncoding.toBase64; -import junit.framework.Assert; +import static org.junit.Assert.assertEquals; import org.junit.Test; import org.slf4j.Logger; @@ -30,66 +30,66 @@ import org.slf4j.LoggerFactory; /** * @author Robert von Burg - * */ +@SuppressWarnings("nls") public class BaseEncodingTest { private static final Logger logger = LoggerFactory.getLogger(BaseEncodingTest.class); @Test public void testBase64() { - Assert.assertEquals("", toBase64("")); - Assert.assertEquals("Zg==", toBase64("f")); - Assert.assertEquals("Zm8=", toBase64("fo")); - Assert.assertEquals("Zm9v", toBase64("foo")); - Assert.assertEquals("Zm9vYg==", toBase64("foob")); - Assert.assertEquals("Zm9vYmE=", toBase64("fooba")); - Assert.assertEquals("Zm9vYmFy", toBase64("foobar")); + assertEquals("", toBase64("")); + assertEquals("Zg==", toBase64("f")); + assertEquals("Zm8=", toBase64("fo")); + assertEquals("Zm9v", toBase64("foo")); + assertEquals("Zm9vYg==", toBase64("foob")); + assertEquals("Zm9vYmE=", toBase64("fooba")); + assertEquals("Zm9vYmFy", toBase64("foobar")); } @Test public void testBase32() { - Assert.assertEquals("", toBase32("")); - Assert.assertEquals("MY======", toBase32("f")); - Assert.assertEquals("MZXQ====", toBase32("fo")); - Assert.assertEquals("MZXW6===", toBase32("foo")); - Assert.assertEquals("MZXW6YQ=", toBase32("foob")); - Assert.assertEquals("MZXW6YTB", toBase32("fooba")); - Assert.assertEquals("MZXW6YTBOI======", toBase32("foobar")); + assertEquals("", toBase32("")); + assertEquals("MY======", toBase32("f")); + assertEquals("MZXQ====", toBase32("fo")); + assertEquals("MZXW6===", toBase32("foo")); + assertEquals("MZXW6YQ=", toBase32("foob")); + assertEquals("MZXW6YTB", toBase32("fooba")); + assertEquals("MZXW6YTBOI======", toBase32("foobar")); } @Test public void testBase32Hex() { - Assert.assertEquals("", toBase32Hex("")); - Assert.assertEquals("CO======", toBase32Hex("f")); - Assert.assertEquals("CPNG====", toBase32Hex("fo")); - Assert.assertEquals("CPNMU===", toBase32Hex("foo")); - Assert.assertEquals("CPNMUOG=", toBase32Hex("foob")); - Assert.assertEquals("CPNMUOJ1", toBase32Hex("fooba")); - Assert.assertEquals("CPNMUOJ1E8======", toBase32Hex("foobar")); + assertEquals("", toBase32Hex("")); + assertEquals("CO======", toBase32Hex("f")); + assertEquals("CPNG====", toBase32Hex("fo")); + assertEquals("CPNMU===", toBase32Hex("foo")); + assertEquals("CPNMUOG=", toBase32Hex("foob")); + assertEquals("CPNMUOJ1", toBase32Hex("fooba")); + assertEquals("CPNMUOJ1E8======", toBase32Hex("foobar")); } @Test public void testBase32Dmedia() { - Assert.assertEquals("", toBase32Dmedia("")); - Assert.assertEquals("FCNPVRELI7J9FUUI", toBase32Dmedia("binary foo")); - Assert.assertEquals("FR======", toBase32Dmedia("f")); - Assert.assertEquals("FSQJ====", toBase32Dmedia("fo")); - Assert.assertEquals("FSQPX===", toBase32Dmedia("foo")); - Assert.assertEquals("FSQPXRJ=", toBase32Dmedia("foob")); - Assert.assertEquals("FSQPXRM4", toBase32Dmedia("fooba")); - Assert.assertEquals("FSQPXRM4HB======", toBase32Dmedia("foobar")); + assertEquals("", toBase32Dmedia("")); + assertEquals("FCNPVRELI7J9FUUI", toBase32Dmedia("binary foo")); + assertEquals("FR======", toBase32Dmedia("f")); + assertEquals("FSQJ====", toBase32Dmedia("fo")); + assertEquals("FSQPX===", toBase32Dmedia("foo")); + assertEquals("FSQPXRJ=", toBase32Dmedia("foob")); + assertEquals("FSQPXRM4", toBase32Dmedia("fooba")); + assertEquals("FSQPXRM4HB======", toBase32Dmedia("foobar")); } @Test public void testBase16() { - Assert.assertEquals("", toBase16("")); - Assert.assertEquals("66", toBase16("f")); - Assert.assertEquals("666F", toBase16("fo")); - Assert.assertEquals("666F6F", toBase16("foo")); - Assert.assertEquals("666F6F62", toBase16("foob")); - Assert.assertEquals("666F6F6261", toBase16("fooba")); - Assert.assertEquals("666F6F626172", toBase16("foobar")); + assertEquals("", toBase16("")); + assertEquals("66", toBase16("f")); + assertEquals("666F", toBase16("fo")); + assertEquals("666F6F", toBase16("foo")); + assertEquals("666F6F62", toBase16("foob")); + assertEquals("666F6F6261", toBase16("fooba")); + assertEquals("666F6F626172", toBase16("foobar")); } @Test From 4df40f28853f5d64ebe6a3194d5c1ee463c21ad4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 25 Dec 2013 11:55:59 +0100 Subject: [PATCH 062/180] [Minor] fixed issue where eclipse couldn't validate log4j.xml --- src/test/resources/log4j.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/resources/log4j.xml b/src/test/resources/log4j.xml index a35a3c351..0a2a73d06 100644 --- a/src/test/resources/log4j.xml +++ b/src/test/resources/log4j.xml @@ -1,5 +1,6 @@ - + From 2e2263128f43d57d242277508a02f7e312ce2761 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 25 Dec 2013 14:35:30 +0100 Subject: [PATCH 063/180] [New] code cleanup and new hashing methods - Cleaned up all i18n compiler warnings in all classes - Added new public hashing methods to pass the hashAlgorithm - Important additional change: HASH table is now lower case. --- .../ch/eitchnet/utils/helper/ByteHelper.java | 28 ++-- .../ch/eitchnet/utils/helper/ClassHelper.java | 81 ++++++++- .../eitchnet/utils/helper/ProcessHelper.java | 154 ++++++++++-------- .../utils/helper/PropertiesHelper.java | 9 +- .../eitchnet/utils/helper/StringHelper.java | 148 ++++++++++++----- .../eitchnet/utils/helper/SystemHelper.java | 45 ++--- .../utils/objectfilter/ObjectCache.java | 26 ++- .../GenerateReverseBaseEncodingAlphabets.java | 3 +- .../utils/objectfilter/ObjectFilterTest.java | 18 +- 9 files changed, 340 insertions(+), 172 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java index b9d4170c3..c9657a51d 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java @@ -33,7 +33,7 @@ public class ByteHelper { public static long toLong(byte[] bytes) { if (bytes.length != 8) - throw new IllegalArgumentException("The input byte array for a long must have 8 values"); + 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) // @@ -57,7 +57,7 @@ public class ByteHelper { public static int toInt(byte[] bytes) { if (bytes.length != 4) - throw new IllegalArgumentException("The input byte array for a long must have 4 values"); + 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) // @@ -92,7 +92,7 @@ public class ByteHelper { /** * Formats the given byte array to a binary string, separating each byte by a space * - * @param b + * @param bytes * the byte to format to a binary string * * @return the binary string @@ -102,7 +102,7 @@ public class ByteHelper { for (byte b : bytes) { sb.append(asBinary(b)); - sb.append(" "); + sb.append(StringHelper.SPACE); } return sb.toString(); @@ -129,7 +129,7 @@ public class ByteHelper { sb.append(((i >>> 25) & 1)); sb.append(((i >>> 24) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 23) & 1)); sb.append(((i >>> 22) & 1)); @@ -140,7 +140,7 @@ public class ByteHelper { sb.append(((i >>> 17) & 1)); sb.append(((i >>> 16) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 15) & 1)); sb.append(((i >>> 14) & 1)); @@ -151,7 +151,7 @@ public class ByteHelper { sb.append(((i >>> 9) & 1)); sb.append(((i >>> 8) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 7) & 1)); sb.append(((i >>> 6) & 1)); @@ -186,7 +186,7 @@ public class ByteHelper { sb.append(((i >>> 57) & 1)); sb.append(((i >>> 56) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 55) & 1)); sb.append(((i >>> 54) & 1)); @@ -197,7 +197,7 @@ public class ByteHelper { sb.append(((i >>> 49) & 1)); sb.append(((i >>> 48) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 47) & 1)); sb.append(((i >>> 46) & 1)); @@ -208,7 +208,7 @@ public class ByteHelper { sb.append(((i >>> 41) & 1)); sb.append(((i >>> 40) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 39) & 1)); sb.append(((i >>> 38) & 1)); @@ -219,7 +219,7 @@ public class ByteHelper { sb.append(((i >>> 33) & 1)); sb.append(((i >>> 32) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 31) & 1)); sb.append(((i >>> 30) & 1)); @@ -230,7 +230,7 @@ public class ByteHelper { sb.append(((i >>> 25) & 1)); sb.append(((i >>> 24) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 23) & 1)); sb.append(((i >>> 22) & 1)); @@ -241,7 +241,7 @@ public class ByteHelper { sb.append(((i >>> 17) & 1)); sb.append(((i >>> 16) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 15) & 1)); sb.append(((i >>> 14) & 1)); @@ -252,7 +252,7 @@ public class ByteHelper { sb.append(((i >>> 9) & 1)); sb.append(((i >>> 8) & 1)); - sb.append(" "); + sb.append(StringHelper.SPACE); sb.append(((i >>> 7) & 1)); sb.append(((i >>> 6) & 1)); diff --git a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java index c25fece89..b24d3cd9f 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java @@ -24,16 +24,81 @@ import java.text.MessageFormat; */ 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) { - + public static T instantiateClass(String className) throws IllegalArgumentException { try { - Class clazz = Class.forName(className); - return (T) clazz.newInstance(); + + Class clazz = (Class) Class.forName(className); + + return clazz.getConstructor().newInstance(); + } catch (Exception e) { - String msg = "Failed to load class {0} due to error: {1}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, className, e.getMessage()); - throw new IllegalArgumentException(msg); + 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/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 6c8b908f0..a4e3f3294 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -19,6 +19,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.text.MessageFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,55 +33,62 @@ public class ProcessHelper { public static ProcessResult runCommand(String command) { final StringBuffer sb = new StringBuffer(); - sb.append("=====================================\n"); + sb.append("=====================================\n"); //$NON-NLS-1$ try { final Process process = Runtime.getRuntime().exec(command); + final int[] returnValue = new int[1]; - final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - Thread errorIn = new Thread("errorIn") { - @Override - public void run() { - ProcessHelper.readStream(sb, "[ERROR] ", errorStream); - } - }; - errorIn.start(); + try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); + final BufferedReader inputStream = new BufferedReader(new InputStreamReader( + process.getInputStream()));) { - final BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream())); - Thread infoIn = new Thread("infoIn") { - @Override - public void run() { - ProcessHelper.readStream(sb, "[INFO] ", inputStream); - } - }; - infoIn.start(); + Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ + @Override + public void run() { + readStream(sb, "[ERROR] ", errorStream); //$NON-NLS-1$ + } + }; + errorIn.start(); - int returnValue = process.waitFor(); + Thread infoIn = new Thread("infoIn") { //$NON-NLS-1$ + @Override + public void run() { + readStream(sb, "[INFO] ", inputStream); //$NON-NLS-1$ + } + }; + infoIn.start(); - errorIn.join(100l); - infoIn.join(100l); - sb.append("=====================================\n"); + returnValue[0] = process.waitFor(); - return new ProcessResult(returnValue, sb.toString(), null); + errorIn.join(100l); + infoIn.join(100l); + sb.append("=====================================\n"); //$NON-NLS-1$ + } + return new ProcessResult(returnValue[0], sb.toString(), null); } catch (IOException e) { - throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); + String msg = MessageFormat.format("Failed to perform command: {0}", e.getMessage()); //$NON-NLS-1$ + throw new RuntimeException(msg, e); } catch (InterruptedException e) { - ProcessHelper.logger.error("Interrupted!"); - sb.append("[FATAL] Interrupted"); + logger.error("Interrupted!"); //$NON-NLS-1$ + sb.append("[FATAL] Interrupted"); //$NON-NLS-1$ return new ProcessResult(-1, sb.toString(), e); } } public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { - if (!workingDirectory.exists()) - throw new RuntimeException("Working directory does not exist at " + workingDirectory.getAbsolutePath()); + if (!workingDirectory.exists()) { + String msg = "Working directory does not exist 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!"); + throw new RuntimeException("No command passed!"); //$NON-NLS-1$ final StringBuffer sb = new StringBuffer(); - sb.append("=====================================\n"); + sb.append("=====================================\n"); //$NON-NLS-1$ try { ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs); @@ -88,38 +96,42 @@ public class ProcessHelper { processBuilder.directory(workingDirectory); final Process process = processBuilder.start(); + int[] returnValue = new int[1]; - final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - Thread errorIn = new Thread("errorIn") { - @Override - public void run() { - ProcessHelper.readStream(sb, "[ERROR] ", errorStream); - } - }; - errorIn.start(); + try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); + final BufferedReader inputStream = new BufferedReader(new InputStreamReader( + process.getInputStream()));) { - final BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream())); - Thread infoIn = new Thread("infoIn") { - @Override - public void run() { - ProcessHelper.readStream(sb, "[INFO] ", inputStream); - } - }; - infoIn.start(); + Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ + @Override + public void run() { + readStream(sb, "[ERROR] ", errorStream); //$NON-NLS-1$ + } + }; + errorIn.start(); - int returnValue = process.waitFor(); + Thread infoIn = new Thread("infoIn") { //$NON-NLS-1$ + @Override + public void run() { + readStream(sb, "[INFO] ", inputStream); //$NON-NLS-1$ + } + }; + infoIn.start(); - errorIn.join(100l); - infoIn.join(100l); - sb.append("=====================================\n"); + returnValue[0] = process.waitFor(); - return new ProcessResult(returnValue, sb.toString(), null); + errorIn.join(100l); + infoIn.join(100l); + sb.append("=====================================\n"); //$NON-NLS-1$ + } + + return new ProcessResult(returnValue[0], sb.toString(), null); } catch (IOException e) { - throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); + throw new RuntimeException("Failed to perform command: " + e.getLocalizedMessage(), e); //$NON-NLS-1$ } catch (InterruptedException e) { - ProcessHelper.logger.error("Interrupted!"); - sb.append("[FATAL] Interrupted"); + logger.error("Interrupted!"); //$NON-NLS-1$ + sb.append("[FATAL] Interrupted"); //$NON-NLS-1$ return new ProcessResult(-1, sb.toString(), e); } } @@ -127,24 +139,27 @@ public class ProcessHelper { public static class ProcessResult { public final int returnValue; public final String processOutput; - public final Throwable t; + public final Throwable throwable; public ProcessResult(int returnValue, String processOutput, Throwable t) { this.returnValue = returnValue; this.processOutput = processOutput; - this.t = t; + this.throwable = t; } } - private static void readStream(StringBuffer sb, String prefix, BufferedReader bufferedReader) { + static void readStream(StringBuffer sb, String prefix, BufferedReader bufferedReader) { String line; try { while ((line = bufferedReader.readLine()) != null) { - sb.append(prefix + line + "\n"); + sb.append(prefix + line + StringHelper.NEW_LINE); } } catch (IOException e) { - String msg = "Faild to read from " + prefix + " stream: " + e.getLocalizedMessage(); - sb.append("[FATAL] " + msg + "\n"); + 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); } } @@ -152,31 +167,32 @@ public class ProcessHelper { ProcessResult processResult; if (SystemHelper.isLinux()) { - processResult = ProcessHelper.runCommand("xdg-open " + pdfPath.getAbsolutePath()); + processResult = runCommand("xdg-open " + pdfPath.getAbsolutePath()); //$NON-NLS-1$ } else if (SystemHelper.isMacOS()) { - processResult = ProcessHelper.runCommand("open " + pdfPath.getAbsolutePath()); + 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 = ProcessHelper.runCommand("rundll32 url.dll,FileProtocolHandler " + pdfFile); + processResult = runCommand("rundll32 url.dll,FileProtocolHandler " + pdfFile); //$NON-NLS-1$ } else { - throw new UnsupportedOperationException("Unexpected OS: " + SystemHelper.osName); + String msg = MessageFormat.format("Unexpected OS: {0}", SystemHelper.osName); //$NON-NLS-1$ + throw new UnsupportedOperationException(msg); } - ProcessHelper.logProcessResult(processResult); + logProcessResult(processResult); } public static void logProcessResult(ProcessResult processResult) { if (processResult.returnValue == 0) { - ProcessHelper.logger.info("Process executed successfully"); + logger.info("Process executed successfully"); //$NON-NLS-1$ } else if (processResult.returnValue == -1) { - ProcessHelper.logger.error("Process execution failed:\n" + processResult.processOutput); - ProcessHelper.logger.error(processResult.t.getMessage(), processResult.t); + logger.error("Process execution failed:\n" + processResult.processOutput); //$NON-NLS-1$ + logger.error(processResult.throwable.getMessage(), processResult.throwable); } else { - ProcessHelper.logger.info("Process execution was not successful with return value:" - + processResult.returnValue + "\n" + processResult.processOutput); + 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/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java index 4e0056492..f09f66e6c 100644 --- a/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java @@ -15,11 +15,11 @@ */ package ch.eitchnet.utils.helper; +import java.text.MessageFormat; import java.util.Properties; /** * @author Robert von Burg - * */ public class PropertiesHelper { @@ -45,8 +45,11 @@ public class PropertiesHelper { public static String getProperty(Properties properties, String context, String key, String def) throws RuntimeException { String property = properties.getProperty(key, def); - if (property == null) - throw new RuntimeException("[" + context + "] Property " + key + " is not set, and no default was given!"); + 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; } diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index b322d57cc..a7eca17de 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -18,6 +18,7 @@ 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; @@ -32,15 +33,17 @@ 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$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); /** - * Hex char table for fast calculating of hex value + * Hex char table for fast calculating of hex values */ - private static final byte[] HEX_CHAR_TABLE = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', - 'D', 'E', 'F' }; + 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 @@ -59,14 +62,15 @@ public class StringHelper { for (byte b : raw) { int v = b & 0xFF; - hex[index++] = StringHelper.HEX_CHAR_TABLE[v >>> 4]; - hex[index++] = StringHelper.HEX_CHAR_TABLE[v & 0xF]; + hex[index++] = HEX_CHAR_TABLE[v >>> 4]; + hex[index++] = HEX_CHAR_TABLE[v & 0xF]; } - return new String(hex, "ASCII"); + return new String(hex, "ASCII"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Something went wrong while converting to HEX: " + e.getLocalizedMessage(), e); + String msg = MessageFormat.format("Something went wrong while converting to HEX: {0}", e.getMessage()); //$NON-NLS-1$ + throw new RuntimeException(msg, e); } } @@ -80,7 +84,7 @@ public class StringHelper { */ public static byte[] fromHexString(String encoded) { if ((encoded.length() % 2) != 0) - throw new IllegalArgumentException("Input string must contain an even number of characters."); + 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(); @@ -102,7 +106,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static String hashMd5AsHex(String string) { - return getHexString(StringHelper.hashMd5(string.getBytes())); + return getHexString(hashMd5(string.getBytes())); } /** @@ -115,7 +119,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(String string) { - return StringHelper.hashMd5(string.getBytes()); + return hashMd5(string.getBytes()); } /** @@ -128,7 +132,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashMd5(byte[] bytes) { - return StringHelper.hash("MD5", bytes); + return hash("MD5", bytes); //$NON-NLS-1$ } /** @@ -140,7 +144,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static String hashSha1AsHex(String string) { - return getHexString(StringHelper.hashSha1(string.getBytes())); + return getHexString(hashSha1(string.getBytes())); } /** @@ -153,7 +157,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(String string) { - return StringHelper.hashSha1(string.getBytes()); + return hashSha1(string.getBytes()); } /** @@ -166,7 +170,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha1(byte[] bytes) { - return StringHelper.hash("SHA-1", bytes); + return hash("SHA-1", bytes); //$NON-NLS-1$ } /** @@ -178,7 +182,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static String hashSha256AsHex(String string) { - return getHexString(StringHelper.hashSha256(string.getBytes())); + return getHexString(hashSha256(string.getBytes())); } /** @@ -191,7 +195,7 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(String string) { - return StringHelper.hashSha256(string.getBytes()); + return hashSha256(string.getBytes()); } /** @@ -204,7 +208,59 @@ public class StringHelper { * @return the hash or null, if an exception was thrown */ public static byte[] hashSha256(byte[] bytes) { - return StringHelper.hash("SHA-256", 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)); } /** @@ -226,7 +282,8 @@ public class StringHelper { return hashArray; } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Algorithm " + algorithm + " does not exist!", e); + String msg = MessageFormat.format("Algorithm {0} does not exist!", algorithm); //$NON-NLS-1$ + throw new RuntimeException(msg, e); } } @@ -245,7 +302,7 @@ public class StringHelper { * @return the new string */ public static String normalizeLength(String value, int length, boolean beginning, char c) { - return StringHelper.normalizeLength(value, length, beginning, false, c); + return normalizeLength(value, length, beginning, false, c); } /** @@ -284,8 +341,8 @@ public class StringHelper { } else if (shorten) { - StringHelper.logger.warn("Shortening length of value: " + value); - StringHelper.logger.warn("Length is: " + value.length() + " max: " + length); + 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); } @@ -300,7 +357,7 @@ public class StringHelper { * returned */ public static String replaceSystemPropertiesIn(String value) { - return StringHelper.replacePropertiesIn(System.getProperties(), value); + return replacePropertiesIn(System.getProperties(), value); } /** @@ -315,10 +372,10 @@ public class StringHelper { * * @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 alue) { + public static String replacePropertiesIn(Properties properties, String value) { // get a copy of the value - String tmpValue = alue; + String tmpValue = value; // get first occurrence of $ character int pos = -1; @@ -337,8 +394,8 @@ public class StringHelper { // if no stop found, then break as another sequence should be able to start if (stop == -1) { - StringHelper.logger.error("Sequence starts at offset " + pos + " but does not end!"); - tmpValue = alue; + logger.error(MessageFormat.format("Sequence starts at offset {0} but does not end!", pos)); //$NON-NLS-1$ + tmpValue = value; break; } @@ -346,15 +403,16 @@ public class StringHelper { String sequence = tmpValue.substring(pos + 2, stop); // make sure sequence doesn't contain $ { } characters - if (sequence.contains("$") || sequence.contains("{") || sequence.contains("}")) { - StringHelper.logger.error("Enclosed sequence in offsets " + pos + " - " + stop - + " contains one of the illegal chars: $ { }: " + sequence); - tmpValue = alue; + if (sequence.contains("$") || 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}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, pos, stop, sequence); + logger.error(msg); + tmpValue = value; break; } // sequence is good, so see if we have a property for it - String property = properties.getProperty(sequence, ""); + String property = properties.getProperty(sequence, StringHelper.EMPTY); // if no property exists, then log and continue if (property.isEmpty()) { @@ -363,7 +421,7 @@ public class StringHelper { } // property exists, so replace in value - tmpValue = tmpValue.replace("${" + sequence + "}", property); + tmpValue = tmpValue.replace("${" + sequence + "}", property); //$NON-NLS-1$ //$NON-NLS-2$ } return tmpValue; @@ -377,7 +435,7 @@ public class StringHelper { * the properties in which the values must have any ${...} replaced by values of the respective key */ public static void replaceProperties(Properties properties) { - StringHelper.replaceProperties(properties, null); + replaceProperties(properties, null); } /** @@ -394,7 +452,7 @@ public class StringHelper { for (Object keyObj : properties.keySet()) { String key = (String) keyObj; String property = properties.getProperty(key); - String newProperty = StringHelper.replacePropertiesIn(properties, property); + String newProperty = replacePropertiesIn(properties, property); // try first properties if (!property.equals(newProperty)) { @@ -403,7 +461,7 @@ public class StringHelper { } else if (altProperties != null) { // try alternative properties - newProperty = StringHelper.replacePropertiesIn(altProperties, property); + 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); @@ -443,10 +501,15 @@ public class StringHelper { 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 + ". Showing " + maxContext - + " extra characters and start and end:\n"); - sb.append("context s1: " + s1.substring(start, end) + "\n"); - sb.append("context s2: " + s2.substring(start, end) + "\n"); + 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(); } @@ -519,15 +582,16 @@ public class StringHelper { */ 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"); + 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 { - throw new RuntimeException("Value " + value - + " can not be parsed to boolean! Expected case insensitive true or false"); + 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); } } } diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index f0fa37734..3b72d38d1 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -32,11 +32,11 @@ public class SystemHelper { return SystemHelper.instance; } - public static final String osName = System.getProperty("os.name"); - public static final String osArch = System.getProperty("os.arch"); - public static final String osVersion = System.getProperty("os.version"); - public static final String javaVendor = System.getProperty("java.vendor"); - public static final String javaVersion = System.getProperty("java.version"); + 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 @@ -59,40 +59,41 @@ public class SystemHelper { public static String asString() { StringBuilder sb = new StringBuilder(); sb.append(SystemHelper.osName); - sb.append(" "); + sb.append(StringHelper.EMPTY); sb.append(SystemHelper.osArch); - sb.append(" "); + sb.append(StringHelper.EMPTY); sb.append(SystemHelper.osVersion); - sb.append(" "); - sb.append("on Java " + SystemHelper.javaVendor); - sb.append(" version "); + sb.append(StringHelper.EMPTY); + 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"); + return System.getProperty("user.dir"); //$NON-NLS-1$ } public static boolean isMacOS() { - return SystemHelper.osName.startsWith("Mac"); + return SystemHelper.osName.startsWith("Mac"); //$NON-NLS-1$ } public static boolean isWindows() { - return SystemHelper.osName.startsWith("Win"); + return SystemHelper.osName.startsWith("Win"); //$NON-NLS-1$ } public static boolean isLinux() { - return SystemHelper.osName.startsWith("Lin"); + return SystemHelper.osName.startsWith("Lin"); //$NON-NLS-1$ } public static boolean is32bit() { - return SystemHelper.osArch.equals("x86") || SystemHelper.osArch.equals("i386") - || SystemHelper.osArch.equals("i686"); + 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"); + return SystemHelper.osArch.equals("x86_64") || SystemHelper.osArch.equals("amd64"); //$NON-NLS-1$ //$NON-NLS-2$ } public static String getMaxMemory() { @@ -108,7 +109,13 @@ public class SystemHelper { } public static String getMemorySummary() { - return "Memory available " + SystemHelper.getMaxMemory() + " / Used: " + SystemHelper.getUsedMemory() - + " / Free:" + SystemHelper.getFreeMemory(); + 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/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java index 1a9bf9fde..8739e86d1 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java @@ -15,6 +15,8 @@ */ package ch.eitchnet.utils.objectfilter; +import java.text.MessageFormat; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,6 +71,7 @@ public class ObjectCache { * @param object * @param operation */ + @SuppressWarnings("nls") public ObjectCache(long id, String key, Object object, Operation operation) { this.id = id; @@ -77,8 +80,16 @@ public class ObjectCache { this.operation = operation; if (logger.isDebugEnabled()) { - logger.debug("Instanciated Cache: ID" + this.id + " / " + key + " OP: " + this.operation - + " / " + object.toString()); + StringBuilder sb = new StringBuilder(); + sb.append("Instanciated Cache: ID"); + sb.append(this.id); + sb.append(" / "); + sb.append(key); + sb.append(" OP: "); + sb.append(this.operation); + sb.append(" / "); + sb.append(object.toString()); + logger.debug(sb.toString()); } } @@ -89,7 +100,7 @@ public class ObjectCache { */ public void setObject(Object object) { if (logger.isDebugEnabled()) { - logger.debug("Updating ID " + this.id + " to value " + object.toString()); + logger.debug(MessageFormat.format("Updating ID {0} to value {1}", this.id, object.toString())); //$NON-NLS-1$ } this.object = object; } @@ -100,9 +111,10 @@ public class ObjectCache { * @param newOperation */ public void setOperation(Operation newOperation) { - if (ObjectCache.logger.isDebugEnabled()) { - ObjectCache.logger.debug("Updating Operation of ID " + this.id + " from " + this.operation + " to " - + newOperation); + if (logger.isDebugEnabled()) { + String msg = "Updating Operation of ID {0} from {1} to {2}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, this.id, this.operation, newOperation); + logger.debug(msg); } this.operation = newOperation; } @@ -120,7 +132,7 @@ public class ObjectCache { public String getKey() { return this.key; } - + /** * @return the operation */ diff --git a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java index 562368b75..5a492a2d7 100644 --- a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java +++ b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java @@ -19,10 +19,11 @@ import java.util.HashMap; import java.util.Map; /** - * Simple helper class to generate the reverse alphabets for {@link BaseDecoding} + * Simple helper class to generate the reverse alphabets for {@link BaseEncoding} * * @author Robert von Burg */ +@SuppressWarnings("nls") public class GenerateReverseBaseEncodingAlphabets { public static void main(String[] args) { diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java index f7a7c99df..70b6c98f7 100644 --- a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -25,8 +25,8 @@ import org.junit.Test; /** * @author Robert von Burg - * */ +@SuppressWarnings("nls") public class ObjectFilterTest { @Test @@ -100,9 +100,9 @@ public class ObjectFilterTest { try { filter.add(myObj); - fail("Should have failed adding twice!"); + fail("Should have failed adding twice!"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid + after +", e.getMessage()); + assertEquals("Stale State exception: Invalid + after +", e.getMessage()); } testAssertions(filter, 1, 1, 1, 0, 0); @@ -118,9 +118,9 @@ public class ObjectFilterTest { try { filter.remove(myObj); - fail("Should have failed removing twice!"); + fail("Should have failed removing twice!"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid - after -", e.getMessage()); + assertEquals("Stale State exception: Invalid - after -", e.getMessage()); } testAssertions(filter, 1, 1, 0, 0, 1); @@ -158,9 +158,9 @@ public class ObjectFilterTest { try { filter.add(myObj); - fail("Should have failed add after modify"); + fail("Should have failed add after modify"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid + after +=", e.getMessage()); + assertEquals("Stale State exception: Invalid + after +=", e.getMessage()); } testAssertions(filter, 1, 1, 0, 1, 0); @@ -186,9 +186,9 @@ public class ObjectFilterTest { try { filter.update(myObj); - fail("Should have failed modify after remove"); + fail("Should have failed modify after remove"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid += after -", e.getMessage()); + assertEquals("Stale State exception: Invalid += after -", e.getMessage()); } testAssertions(filter, 1, 1, 0, 0, 1); From d677249cf6b67eb43052c670031bc12ed09c2a64 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 31 Dec 2013 19:56:45 +0100 Subject: [PATCH 064/180] [Minor] added StringHelper.DASH constant --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index a7eca17de..45849de3c 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -35,6 +35,7 @@ public class StringHelper { 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 = "dash"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); From 7202545f6f8ddcba27387c6aa21e19ce207026a1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 2 Jan 2014 16:29:02 +0100 Subject: [PATCH 065/180] [Bugfix] fixed wrong constant value --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 45849de3c..d9eea196d 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -35,7 +35,7 @@ public class StringHelper { 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 = "dash"; //$NON-NLS-1$ + public static final String DASH = "-"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); From 48e2defc9eaef6709d092754be53d7280422d19f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 10 Jan 2014 23:13:41 +0100 Subject: [PATCH 066/180] [Bugfix] fixed a bug where FileHelper.copy() didn't recursively copy --- .../java/ch/eitchnet/utils/helper/FileHelper.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 862598d9b..eb0725308 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -234,8 +234,9 @@ public class FileHelper { } /** - * Copy a given list of {@link File Files}. The renameTo method does not allow action across NFS mounted filesystems - * this method is the workaround + *

+ * Copy a given list of {@link File Files}. Recursively copies the files and directories to the destination. + *

* * @param srcFiles * The source files to copy @@ -255,8 +256,14 @@ public class FileHelper { for (File srcFile : srcFiles) { File dstFile = new File(dstDirectory, srcFile.getName()); - if (!copy(srcFile, dstFile, checksum)) - return false; + if (srcFile.isDirectory()) { + dstFile.mkdir(); + if (!copy(srcFile.listFiles(), dstFile, checksum)) + return false; + } else { + if (!copy(srcFile, dstFile, checksum)) + return false; + } } return true; From 4b179f8bb44f3ef4eb03ec91c2e242244c81df7b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 13 Jan 2014 21:09:28 +0100 Subject: [PATCH 067/180] [Minor] small error handling change in FileHelper --- .../ch/eitchnet/utils/helper/FileHelper.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index eb0725308..a1ad8cdbd 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -244,7 +244,7 @@ public class FileHelper { * 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 renaming succeeded; false otherwise + * @return true if and only if the copying succeeded; false otherwise */ public final static boolean copy(File[] srcFiles, File dstDirectory, boolean checksum) { @@ -258,11 +258,16 @@ public class FileHelper { File dstFile = new File(dstDirectory, srcFile.getName()); if (srcFile.isDirectory()) { dstFile.mkdir(); - if (!copy(srcFile.listFiles(), dstFile, checksum)) + if (!copy(srcFile.listFiles(), dstFile, checksum)) { + String msg = "Failed to copy contents of {0} to {1}"; + msg = MessageFormat.format(msg, srcFile.getAbsolutePath(), dstFile.getAbsolutePath()); + logger.error(msg); return false; + } } else { - if (!copy(srcFile, dstFile, checksum)) + if (!copy(srcFile, dstFile, checksum)) { return false; + } } } @@ -310,8 +315,8 @@ public class FileHelper { // cleanup if files are not the same length if (fromFile.length() != toFile.length()) { - String msg = MessageFormat.format("Copying failed, as new files are not the same length: {0} / {1}", //$NON-NLS-1$ - fromFile.length(), toFile.length()); + String msg = "Copying failed, as new files are not the same length: {0} / {1}"; + msg = MessageFormat.format(msg, fromFile.length(), toFile.length()); FileHelper.logger.error(msg); toFile.delete(); @@ -319,8 +324,8 @@ public class FileHelper { } } catch (Exception e) { - - FileHelper.logger.error(e.getMessage(), e); + String msg = MessageFormat.format("Failed to copy path from {0} to + {1} due to:", fromFile, toFile); + FileHelper.logger.error(msg, e); return false; } From adc9e3598c9e2b4eee7f76b18a8cada37031436b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 15 Jan 2014 22:02:21 +0100 Subject: [PATCH 068/180] [New] new DBC.PRE.assertNull() method --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 3ea0bb26e..9f83f31f8 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -41,11 +41,24 @@ public enum DBC { throw new DbcException(ex); } } + + @Override + public void assertNull(String msg, Object value) { + if (value != null) { + String ex = "Illegal situation as value is not null: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + }; public abstract void assertNotEmpty(String msg, String value); + public abstract void assertNotNull(String msg, Object value); + public abstract void assertNull(String msg, Object value); + public class DbcException extends RuntimeException { private static final long serialVersionUID = 1L; From c969e25a858cc026da5259a1fd68b4d84f550bf4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 21 Jan 2014 07:40:27 +0100 Subject: [PATCH 069/180] [New] Added StringHelper.formatException(Throwable) --- .../ch/eitchnet/utils/helper/StringHelper.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index d9eea196d..db43eb557 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -15,6 +15,8 @@ */ package ch.eitchnet.utils.helper; +import java.io.PrintWriter; +import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -551,6 +553,21 @@ public class StringHelper { } } + /** + * 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(); + } + /** * Simply returns true if the value is null, or empty * From bfe08c9dc9dc4dda48d3b5cb6410002a04eee994 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 23 Jan 2014 22:52:25 +0100 Subject: [PATCH 070/180] [Project] added Jenkins build badge to README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ed357451..7b9074980 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -ch.eitchnet.java.utils +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 From dfc9d7ab178b8d95f3d4d9a08133b2dadaf00aa9 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 30 Jan 2014 00:20:27 +0100 Subject: [PATCH 071/180] [New] added DBC.PRE.assert*Empty(File) --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 9f83f31f8..9a84ec46c 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -15,6 +15,7 @@ */ package ch.eitchnet.utils.dbc; +import java.io.File; import java.text.MessageFormat; import ch.eitchnet.utils.helper.StringHelper; @@ -51,6 +52,23 @@ public enum DBC { } } + @Override + public void assertNotExists(String msg, File file) { + if (file.exists()) { + String ex = "Illegal situation as file (" + file + ") exists: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + @Override + public void assertExists(String msg, File file) { + if (!file.exists()) { + String ex = "Illegal situation as file (" + file + ") does not exist: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } }; public abstract void assertNotEmpty(String msg, String value); @@ -59,6 +77,10 @@ public enum DBC { public abstract void assertNull(String msg, Object value); + public abstract void assertNotExists(String msg, File file); + + public abstract void assertExists(String msg, File file); + public class DbcException extends RuntimeException { private static final long serialVersionUID = 1L; From a79c9d88ce625289cc8aab4202eca5dfaaafa68d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 31 Jan 2014 15:56:16 +0100 Subject: [PATCH 072/180] [New] Added StringHelper.UNDERLINE and COMMA --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index db43eb557..4a8bc83aa 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -38,6 +38,8 @@ public class StringHelper { 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$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); From dda9ed8423d5e9c354ca57a75f4915ee47084913 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 31 Jan 2014 18:57:04 +0100 Subject: [PATCH 073/180] [New] added method StringHelper.isNotEmpty() --- .../ch/eitchnet/utils/helper/StringHelper.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 4a8bc83aa..34b3416f1 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -40,6 +40,8 @@ public class StringHelper { 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 SEMICOLON = ";"; //$NON-NLS-1$ + public static final String COLON = ":"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(StringHelper.class); @@ -582,6 +584,18 @@ public class StringHelper { 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 From 7f2b7435a966bf4d5448767c30afb4c5f4e60c78 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 31 Jan 2014 18:58:47 +0100 Subject: [PATCH 074/180] [Minor] code cleanup --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 9a84ec46c..662c2fe0f 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -55,7 +55,7 @@ public enum DBC { @Override public void assertNotExists(String msg, File file) { if (file.exists()) { - String ex = "Illegal situation as file (" + file + ") exists: {0}"; //$NON-NLS-1$ + String ex = MessageFormat.format("Illegal situation as file ({0}) exists: {0}", file); //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -64,7 +64,7 @@ public enum DBC { @Override public void assertExists(String msg, File file) { if (!file.exists()) { - String ex = "Illegal situation as file (" + file + ") does not exist: {0}"; //$NON-NLS-1$ + String ex = MessageFormat.format("Illegal situation as file ({0}) does not exist: {0}", file); //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } From 7ec21839d97f12ac854f1b2f105624d5fc94bf1b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 31 Jan 2014 19:58:24 +0100 Subject: [PATCH 075/180] [Minor] code cleanup --- src/main/java/ch/eitchnet/utils/helper/FileHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index a1ad8cdbd..7d73800ec 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -259,7 +259,7 @@ public class FileHelper { if (srcFile.isDirectory()) { dstFile.mkdir(); if (!copy(srcFile.listFiles(), dstFile, checksum)) { - String msg = "Failed to copy contents of {0} to {1}"; + 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; @@ -315,7 +315,7 @@ public class FileHelper { // 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}"; + 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(); @@ -324,7 +324,7 @@ public class FileHelper { } } catch (Exception e) { - String msg = MessageFormat.format("Failed to copy path from {0} to + {1} due to:", fromFile, toFile); + 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; } From a4b459b11d2c441acf8e5e8a051e2f06987c9a7e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 1 Feb 2014 13:14:29 +0100 Subject: [PATCH 076/180] [Minor] extended DBC with assertTrue and assertFalse --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 662c2fe0f..2e783f2d0 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -26,6 +26,26 @@ import ch.eitchnet.utils.helper.StringHelper; public enum DBC { PRE { + + @Override + public void assertTrue(String msg, boolean value) { + if (!value) { + String ex = "Expected true, but was false: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + @Override + public void assertFalse(String msg, boolean value) { + if (value) { + String ex = "Expected false, but was true: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + @Override public void assertNotEmpty(String msg, String value) { if (StringHelper.isEmpty(value)) { String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ @@ -71,6 +91,10 @@ public enum DBC { } }; + public abstract void assertTrue(String msg, boolean value); + + public abstract void assertFalse(String msg, boolean value); + public abstract void assertNotEmpty(String msg, String value); public abstract void assertNotNull(String msg, Object value); From a8264aca3727c64a629718ce26d4c53781ff62be Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 2 Feb 2014 20:31:04 +0100 Subject: [PATCH 077/180] [New] added DBC.assertEquals() including generated tests --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 22 +- .../java/ch/eitchnet/utils/dbc/DBCTest.java | 380 ++++++++++++++++++ 2 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 src/test/java/ch/eitchnet/utils/dbc/DBCTest.java diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 2e783f2d0..199b2bd29 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -27,6 +27,22 @@ public enum DBC { PRE { + @Override + public void assertEquals(String msg, Object value1, Object value2) { + if (value1 == null && value2 == null) + return; + + if (value1 != null && value1.equals(value2)) + return; + + if (value2 != null && value2.equals(value1)) + return; + + String ex = "Values are not equal: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + @Override public void assertTrue(String msg, boolean value) { if (!value) { @@ -75,7 +91,7 @@ public enum DBC { @Override public void assertNotExists(String msg, File file) { if (file.exists()) { - String ex = MessageFormat.format("Illegal situation as file ({0}) exists: {0}", file); //$NON-NLS-1$ + String ex = MessageFormat.format("Illegal situation as file ({0}) exists: {1}", file, msg); //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -84,13 +100,15 @@ public enum DBC { @Override public void assertExists(String msg, File file) { if (!file.exists()) { - String ex = MessageFormat.format("Illegal situation as file ({0}) does not exist: {0}", file); //$NON-NLS-1$ + String ex = MessageFormat.format("Illegal situation as file ({0}) does not exist: {1}", file, msg); //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } } }; + public abstract void assertEquals(String msg, Object value1, Object value2); + public abstract void assertTrue(String msg, boolean value); public abstract void assertFalse(String msg, boolean value); diff --git a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java new file mode 100644 index 000000000..f0ed48526 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java @@ -0,0 +1,380 @@ +package ch.eitchnet.utils.dbc; + +import java.io.File; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import ch.eitchnet.utils.dbc.DBC.DbcException; + +/** + * The class DBCTest contains tests for the class {@link DBC}. + * + * @generatedBy CodePro at 2/2/14 8:13 PM + * @author eitch + * @version $Revision: 1.0 $ + */ +@SuppressWarnings("nls") +public class DBCTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertEquals_1() throws Exception { + String msg = ""; + Object value1 = null; + Object value2 = null; + + DBC.PRE.assertEquals(msg, value1, value2); + + // add additional test code here + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertEquals_2() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Values are not equal:"); + String msg = ""; + Object value1 = new Object(); + Object value2 = new Object(); + + DBC.PRE.assertEquals(msg, value1, value2); + + // add additional test code here + // An unexpected exception was thrown in user code while executing this test: + // ch.eitchnet.utils.DBC.PRE.DBC$DbcException: Values are not equal: + // at ch.eitchnet.utils.DBC.PRE.DBC.PRE.assertEquals(DBC.PRE.java:39) + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertEquals_3() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Values are not equal:"); + + String msg = ""; + Object value1 = null; + Object value2 = new Object(); + + DBC.PRE.assertEquals(msg, value1, value2); + + // add additional test code here + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertEquals_4() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Values are not equal:"); + + String msg = ""; + Object value1 = new Object(); + Object value2 = null; + + DBC.PRE.assertEquals(msg, value1, value2); + + // add additional test code here + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertEquals_5() throws Exception { + String msg = ""; + Object value1 = "bla"; + Object value2 = "bla"; + + DBC.PRE.assertEquals(msg, value1, value2); + + // add additional test code here + // An unexpected exception was thrown in user code while executing this test: + // ch.eitchnet.utils.DBC.PRE.DBC$DbcException: Values are not equal: + // at ch.eitchnet.utils.DBC.PRE.DBC.PRE.assertEquals(DBC.PRE.java:39) + } + + /** + * Run the void assertExists(String,File) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertExists_1() throws Exception { + String msg = ""; + File file = new File("src"); + + DBC.PRE.assertExists(msg, file); + } + + /** + * Run the void assertExists(String,File) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertExists_2() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Illegal situation as file (srcc) does not exist:"); + + String msg = ""; + File file = new File("srcc"); + + DBC.PRE.assertExists(msg, file); + + // add additional test code here + // An unexpected exception was thrown in user code while executing this test: + // ch.eitchnet.utils.DBC.PRE.DBC$DbcException: Illegal situation as file () does not exist: + // at ch.eitchnet.utils.DBC.PRE.DBC.PRE.assertExists(DBC.PRE.java:95) + } + + /** + * Run the void assertFalse(String,boolean) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertFalse_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Expected false, but was true: "); + + String msg = ""; + boolean value = true; + + DBC.PRE.assertFalse(msg, value); + } + + /** + * Run the void assertFalse(String,boolean) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertFalse_2() throws Exception { + String msg = ""; + boolean value = false; + + DBC.PRE.assertFalse(msg, value); + + // add additional test code here + } + + /** + * Run the void assertNotEmpty(String,String) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEmpty_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Illegal empty value: "); + + String msg = "Illegal empty value: "; + String value = ""; + + DBC.PRE.assertNotEmpty(msg, value); + } + + /** + * Run the void assertNotEmpty(String,String) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEmpty_2() throws Exception { + String msg = ""; + String value = "a"; + + DBC.PRE.assertNotEmpty(msg, value); + } + + /** + * Run the void assertNotExists(String,File) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotExists_1() throws Exception { + String msg = ""; + File file = new File("srcc"); + + DBC.PRE.assertNotExists(msg, file); + + // add additional test code here + } + + /** + * Run the void assertNotExists(String,File) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotExists_2() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Illegal situation as file (src) exists: "); + + String msg = ""; + File file = new File("src"); + + DBC.PRE.assertNotExists(msg, file); + + // add additional test code here + } + + /** + * Run the void assertNotNull(String,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotNull_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Illegal null value:"); + + String msg = ""; + Object value = null; + + DBC.PRE.assertNotNull(msg, value); + } + + /** + * Run the void assertNotNull(String,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotNull_2() throws Exception { + String msg = ""; + Object value = new Object(); + + DBC.PRE.assertNotNull(msg, value); + + // add additional test code here + } + + /** + * Run the void assertNull(String,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNull_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Illegal situation as value is not null:"); + + String msg = ""; + Object value = new Object(); + + DBC.PRE.assertNull(msg, value); + } + + /** + * Run the void assertNull(String,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNull_2() throws Exception { + String msg = ""; + Object value = null; + + DBC.PRE.assertNull(msg, value); + + // add additional test code here + } + + /** + * Run the void assertTrue(String,boolean) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertTrue_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Expected true, but was false: "); + + String msg = ""; + boolean value = false; + + DBC.PRE.assertTrue(msg, value); + + // add additional test code here + // An unexpected exception was thrown in user code while executing this test: + // ch.eitchnet.utils.DBC.PRE.DBC$DbcException: Expected true, but was false: + // at ch.eitchnet.utils.DBC.PRE.DBC.PRE.assertTrue(DBC.PRE.java:47) + } + + /** + * Run the void assertTrue(String,boolean) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertTrue_2() throws Exception { + String msg = ""; + boolean value = true; + + DBC.PRE.assertTrue(msg, value); + + // add additional test code here + } +} \ No newline at end of file From 09444817e2094d9a281ed07983c1ab8aaa267db3 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 2 Feb 2014 20:42:27 +0100 Subject: [PATCH 078/180] [New] added FileProgressListener with ProgressableFileInputStream --- .../utils/io/FileProgressListener.java | 41 +++++ .../utils/io/FileStreamProgressWatcher.java | 81 ++++++++++ .../utils/io/LoggingFileProgressListener.java | 43 ++++++ .../utils/io/ProgressableFileInputStream.java | 146 ++++++++++++++++++ 4 files changed, 311 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/io/FileProgressListener.java create mode 100644 src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java create mode 100644 src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java create mode 100644 src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java diff --git a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java new file mode 100644 index 000000000..b22649b00 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java @@ -0,0 +1,41 @@ +package ch.eitchnet.utils.io; + +/** + *

+ * This interface defines an API for use in situations where long running jobs notify observers of the jobs status. The + * jobs has a size which is a primitive long value e.g. the number of bytes parsed/ to be parsed in a file + *

+ * + * @author Robert von Burg + */ +public interface FileProgressListener { + + /** + * Notify the listener that the progress has begun + * + * @param size + * the size of the job which is to be accomplished + */ + public void begin(long size); + + /** + * Notifies the listener of incremental progress + * + * @param percent + * percent completed + * @param position + * the position relative to the job size + */ + public void progress(int percent, long position); + + /** + * Notifies the listener that the progress is completed + * + * @param percent + * the percent completed. Ideally the value would be 100, but in cases of errors it can be less + * @param position + * the position where the job finished. Ideally the value would be the same as the size given at + * {@link #begin(long)} but in case of errors it can be different + */ + public void end(int percent, long position); +} diff --git a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java new file mode 100644 index 000000000..9ffc13a46 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java @@ -0,0 +1,81 @@ +package ch.eitchnet.utils.io; + +import java.text.MessageFormat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * File stream progress monitoring thread + * + * @author Robert von Burg + */ +public class FileStreamProgressWatcher implements Runnable { + + private static final Logger logger = LoggerFactory.getLogger(FileStreamProgressWatcher.class); + private ProgressableFileInputStream inputStream; + private boolean run = false; + private FileProgressListener progressListener; + private long millis; + + /** + * @param millis + * @param progressListener + * @param inputStream + */ + public FileStreamProgressWatcher(long millis, FileProgressListener progressListener, + ProgressableFileInputStream inputStream) { + this.millis = millis; + this.progressListener = progressListener; + this.inputStream = inputStream; + } + + @Override + public void run() { + this.run = true; + + this.progressListener.begin(this.inputStream.getFileSize()); + + while (this.run) { + try { + + int percentComplete = this.inputStream.getPercentComplete(); + + if (this.inputStream.isClosed()) { + logger.info(MessageFormat.format("Input Stream is closed at: {0}%", percentComplete)); //$NON-NLS-1$ + + this.run = false; + this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); + + } else if (percentComplete < 100) { + + this.progressListener.progress(percentComplete, this.inputStream.getBytesRead()); + + } else if (percentComplete >= 100) { + + this.run = false; + this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); + + } + + if (this.run) { + Thread.sleep(this.millis); + } + + } catch (InterruptedException e) { + + logger.info(MessageFormat.format("Work stopped: {0}", e.getLocalizedMessage())); //$NON-NLS-1$ + this.run = false; + int percentComplete = this.inputStream.getPercentComplete(); + this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); + + } catch (Exception e) { + + logger.error(e.getMessage(), e); + this.run = false; + int percentComplete = this.inputStream.getPercentComplete(); + this.progressListener.end(percentComplete, Long.MAX_VALUE); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java new file mode 100644 index 000000000..4cafa25fd --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java @@ -0,0 +1,43 @@ +package ch.eitchnet.utils.io; + +import java.text.MessageFormat; + +import org.slf4j.Logger; + +import ch.eitchnet.utils.helper.FileHelper; + +/** + * @author Robert von Burg + */ +public class LoggingFileProgressListener implements FileProgressListener { + + private final Logger logger; + private final String name; + + /** + * @param logger + * @param name + */ + public LoggingFileProgressListener(Logger logger, String name) { + this.logger = logger; + this.name = name; + } + + @Override + public void begin(long size) { + String msg = "Starting to read {0} with a size of {1}"; //$NON-NLS-1$ + this.logger.info(MessageFormat.format(msg, this.name, FileHelper.humanizeFileSize(size))); + } + + @Override + public void progress(int percent, long position) { + String msg = "Read {0}% of {1} at position {2}"; //$NON-NLS-1$ + this.logger.info(MessageFormat.format(msg, percent, this.name, FileHelper.humanizeFileSize(position))); + } + + @Override + public void end(int percent, long position) { + String msg = "Finished reading {0} at position {1} ({2}%)"; //$NON-NLS-1$ + this.logger.info(MessageFormat.format(msg, this.name, percent, FileHelper.humanizeFileSize(position))); + } +} diff --git a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java new file mode 100644 index 000000000..94890cc72 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java @@ -0,0 +1,146 @@ +package ch.eitchnet.utils.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + *

+ * This sub class of {@link FileInputStream} allows to follow the currently read bytes of a {@link File}. In conjunction + * with a {@link Thread} and a {@link FileProgressListener} it is possible to track the progress of a long running job on + * bigger files + *

+ * + * @author Robert von Burg + */ +public class ProgressableFileInputStream extends FileInputStream { + + private long fileSize; + private long bytesRead; + private boolean closed; + + /** + * Constructs a normal {@link FileInputStream} with the given {@link File} + * + * @param file + * the file to read + * @throws FileNotFoundException + * thrown if the {@link File} does not exist + */ + public ProgressableFileInputStream(File file) throws FileNotFoundException { + super(file); + this.fileSize = file.length(); + } + + /** + * @see java.io.FileInputStream#read() + */ + @Override + public int read() throws IOException { + synchronized (this) { + this.bytesRead++; + } + return super.read(); + } + + /** + * @see java.io.FileInputStream#read(byte[], int, int) + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = super.read(b, off, len); + if (read != -1) { + synchronized (this) { + this.bytesRead += read; + } + } + return read; + } + + /** + * @see java.io.FileInputStream#read(byte[]) + */ + @Override + public int read(byte[] b) throws IOException { + int read = super.read(b); + if (read != -1) { + synchronized (this) { + this.bytesRead += read; + } + } + return read; + } + + /** + * @see java.io.FileInputStream#skip(long) + */ + @Override + public long skip(long n) throws IOException { + long skip = super.skip(n); + if (skip != -1) { + synchronized (this) { + this.bytesRead += skip; + } + } + return skip; + } + + /** + * @see java.io.FileInputStream#close() + */ + @Override + public void close() throws IOException { + this.closed = true; + super.close(); + } + + /** + * Returns the size of the file being read + * + * @return the size of the file being read + */ + public long getFileSize() { + return this.fileSize; + } + + /** + * Returns the number of bytes already read + * + * @return the number of bytes already read + */ + public long getBytesRead() { + synchronized (this) { + if (this.bytesRead > this.fileSize) + this.bytesRead = this.fileSize; + return this.bytesRead; + } + } + + /** + * Returns the percent read of the file + * + * @return the percentage complete of the process + */ + public int getPercentComplete() { + + long currentRead; + synchronized (this) { + if (this.bytesRead > this.fileSize) + this.bytesRead = this.fileSize; + currentRead = this.bytesRead; + } + + double read = (100.0d / this.fileSize * currentRead); + return (int) read; + } + + /** + * Returns true if {@link #close()} was called, false otherwise + * + * @return the closed + */ + public boolean isClosed() { + return this.closed; + } +} From 50077bb2055054b0e9bb807f9ac645de6fb56f7c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 3 Feb 2014 13:52:59 +0100 Subject: [PATCH 079/180] Update README.md Updated changed dependencies JRE 6 to 7 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7b9074980..46c7a0bcd 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ 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 6 -* JUnit 4.10 (only during tests) +* the Java Runtime Environment 7 +* JUnit 4.11 (test scope) * slf4j 1.7.2 -* slf4j-log4j bindings (only during tests) +* slf4j-log4j bindings (test scope) Features ---------------------- From 4ab52576c0010c0c35cbc41784ba09ec52703729 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 3 Feb 2014 17:54:15 +0100 Subject: [PATCH 080/180] [New] implemented FileProgressListener and ProgressableFileInputStream --- .../utils/io/FileProgressListener.java | 6 +++++- .../utils/io/FileStreamProgressWatcher.java | 21 +++++-------------- .../utils/io/LoggingFileProgressListener.java | 15 ++++++++----- .../utils/io/ProgressableFileInputStream.java | 12 +++++------ 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java index b22649b00..e7bcd4eeb 100644 --- a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java +++ b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java @@ -13,10 +13,14 @@ public interface FileProgressListener { /** * Notify the listener that the progress has begun * + * @param percent + * percent completed + * @param position + * the position relative to the job size * @param size * the size of the job which is to be accomplished */ - public void begin(long size); + public void begin(int percent, long position, long size); /** * Notifies the listener of incremental progress diff --git a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java index 9ffc13a46..b6105a200 100644 --- a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java +++ b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java @@ -33,44 +33,33 @@ public class FileStreamProgressWatcher implements Runnable { @Override public void run() { this.run = true; - - this.progressListener.begin(this.inputStream.getFileSize()); + this.progressListener.begin(this.inputStream.getPercentComplete(), this.inputStream.getBytesRead(), + this.inputStream.getFileSize()); while (this.run) { try { - int percentComplete = this.inputStream.getPercentComplete(); - if (this.inputStream.isClosed()) { logger.info(MessageFormat.format("Input Stream is closed at: {0}%", percentComplete)); //$NON-NLS-1$ - this.run = false; this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); - } else if (percentComplete < 100) { - this.progressListener.progress(percentComplete, this.inputStream.getBytesRead()); - } else if (percentComplete >= 100) { - this.run = false; this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); - } - if (this.run) { + if (this.run) Thread.sleep(this.millis); - } } catch (InterruptedException e) { - - logger.info(MessageFormat.format("Work stopped: {0}", e.getLocalizedMessage())); //$NON-NLS-1$ this.run = false; int percentComplete = this.inputStream.getPercentComplete(); + if (percentComplete != 100) + logger.info(MessageFormat.format("Work stopped: {0}", e.getLocalizedMessage())); //$NON-NLS-1$ this.progressListener.end(percentComplete, this.inputStream.getBytesRead()); - } catch (Exception e) { - logger.error(e.getMessage(), e); this.run = false; int percentComplete = this.inputStream.getPercentComplete(); diff --git a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java index 4cafa25fd..d5d6faf5f 100644 --- a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java +++ b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java @@ -24,20 +24,25 @@ public class LoggingFileProgressListener implements FileProgressListener { } @Override - public void begin(long size) { - String msg = "Starting to read {0} with a size of {1}"; //$NON-NLS-1$ - this.logger.info(MessageFormat.format(msg, this.name, FileHelper.humanizeFileSize(size))); + public void begin(int percent, long position, long size) { + String msg = "Starting to read {0} {1} of {2} ({3}%)"; //$NON-NLS-1$ + log(MessageFormat.format(msg, this.name, position, FileHelper.humanizeFileSize(size), percent)); } @Override public void progress(int percent, long position) { String msg = "Read {0}% of {1} at position {2}"; //$NON-NLS-1$ - this.logger.info(MessageFormat.format(msg, percent, this.name, FileHelper.humanizeFileSize(position))); + log(MessageFormat.format(msg, percent, this.name, FileHelper.humanizeFileSize(position))); } @Override public void end(int percent, long position) { String msg = "Finished reading {0} at position {1} ({2}%)"; //$NON-NLS-1$ - this.logger.info(MessageFormat.format(msg, this.name, percent, FileHelper.humanizeFileSize(position))); + log(MessageFormat.format(msg, this.name, FileHelper.humanizeFileSize(position), percent)); } + + private void log(String msg) { + this.logger.info(msg); + } + } diff --git a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java index 94890cc72..dbd1514ef 100644 --- a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java +++ b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java @@ -8,17 +8,17 @@ import java.io.IOException; /** *

* This sub class of {@link FileInputStream} allows to follow the currently read bytes of a {@link File}. In conjunction - * with a {@link Thread} and a {@link FileProgressListener} it is possible to track the progress of a long running job on - * bigger files + * with a {@link Thread} and a {@link FileProgressListener} it is possible to track the progress of a long running job + * on bigger files *

* * @author Robert von Burg */ public class ProgressableFileInputStream extends FileInputStream { - private long fileSize; - private long bytesRead; - private boolean closed; + private volatile long fileSize; + private volatile long bytesRead; + private volatile boolean closed; /** * Constructs a normal {@link FileInputStream} with the given {@link File} @@ -132,7 +132,7 @@ public class ProgressableFileInputStream extends FileInputStream { } double read = (100.0d / this.fileSize * currentRead); - return (int) read; + return (int) Math.ceil(read); } /** From 221a307c884cf696b372b1ce7fbd83b54b72c22a Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 11 Feb 2014 08:34:23 +0100 Subject: [PATCH 081/180] [New] implemented a MapOfMaps to easily store map of maps --- .../eitchnet/utils/collections/MapOfMaps.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java new file mode 100644 index 000000000..4a6f6ed15 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java @@ -0,0 +1,128 @@ +/* + * 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.HashMap; +import java.util.Iterator; +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: + *

+ * + *
+ * Map<String, Map&lgt;String, MyObject>> mapOfMaps = new HashMap<>;
+ * 
+ * + *

+ * As an example to persist a map of MyObject where the branches are String one would write it as follows: + *

+ * + *
+ * MapOfMaps<String, String, MyObject> mapOfMaps = new MapOfMaps<>();
+ * 
+ * + * + * @author Robert von Burg + * + * @param + * The key to a map with U as the key and V as the value + * @param + * The key to get a value (leaf) + * @param + * The value stored in the tree (leaf) + */ +public class MapOfMaps { + + private Map> mapOfMaps; + + public MapOfMaps() { + this.mapOfMaps = new HashMap<>(); + } + + public Set keySet() { + return this.mapOfMaps.keySet(); + } + + public Map getMap(T t) { + return this.mapOfMaps.get(t); + } + + public V getElement(T t, U u) { + Map map = this.mapOfMaps.get(t); + if (map == null) + return null; + return map.get(u); + } + + public V addElement(T t, U u, V v) { + Map map = this.mapOfMaps.get(t); + if (map == null) { + map = new HashMap<>(); + this.mapOfMaps.put(t, map); + } + return map.put(u, v); + } + + public void addMap(T t, Map u) { + Map map = this.mapOfMaps.get(t); + if (map == null) { + map = new HashMap<>(); + this.mapOfMaps.put(t, map); + } + map.putAll(u); + } + + public V removeElement(T t, U u) { + Map map = this.mapOfMaps.get(t); + if (map == null) { + return null; + } + V v = map.remove(u); + if (map.isEmpty()) { + this.mapOfMaps.remove(t); + } + + return v; + } + + public Map removeMap(T t) { + return this.mapOfMaps.remove(t); + } + + public void clear() { + Set>> entrySet = this.mapOfMaps.entrySet(); + Iterator>> iter = entrySet.iterator(); + while (iter.hasNext()) { + iter.next().getValue().clear(); + iter.remove(); + } + } + + public boolean containsMap(T t) { + return this.mapOfMaps.containsKey(t); + } + + public boolean containsElement(T t, U u) { + Map map = this.mapOfMaps.get(t); + if (map == null) + return false; + return map.containsKey(u); + } +} From 0a6af4a50ee2065c9dd4a405853bef8ca0f34459 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 14 Feb 2014 20:56:06 +0100 Subject: [PATCH 082/180] [Minor] DBC now also has POST and INTERIM --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 131 ++++++++----------- 1 file changed, 53 insertions(+), 78 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 199b2bd29..8aca39191 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -25,103 +25,78 @@ import ch.eitchnet.utils.helper.StringHelper; */ public enum DBC { - PRE { + PRE, INTERIM, POST; - @Override - public void assertEquals(String msg, Object value1, Object value2) { - if (value1 == null && value2 == null) - return; + public void assertEquals(String msg, Object value1, Object value2) { + if (value1 == null && value2 == null) + return; - if (value1 != null && value1.equals(value2)) - return; + if (value1 != null && value1.equals(value2)) + return; - if (value2 != null && value2.equals(value1)) - return; + if (value2 != null && value2.equals(value1)) + return; - String ex = "Values are not equal: {0}"; //$NON-NLS-1$ + String ex = "Values are not equal: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + + public void assertTrue(String msg, boolean value) { + if (!value) { + String ex = "Expected true, but was false: {0}"; //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } + } - @Override - public void assertTrue(String msg, boolean value) { - if (!value) { - String ex = "Expected true, but was false: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertFalse(String msg, boolean value) { + if (value) { + String ex = "Expected false, but was true: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } + } - @Override - public void assertFalse(String msg, boolean value) { - if (value) { - String ex = "Expected false, but was true: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertNotEmpty(String msg, String value) { + if (StringHelper.isEmpty(value)) { + String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } + } - @Override - public void assertNotEmpty(String msg, String value) { - if (StringHelper.isEmpty(value)) { - String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertNotNull(String msg, Object value) { + if (value == null) { + String ex = "Illegal null value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } + } - @Override - public void assertNotNull(String msg, Object value) { - if (value == null) { - String ex = "Illegal null value: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertNull(String msg, Object value) { + if (value != null) { + String ex = "Illegal situation as value is not null: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } + } - @Override - public void assertNull(String msg, Object value) { - if (value != null) { - String ex = "Illegal situation as value is not null: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertNotExists(String msg, File file) { + if (file.exists()) { + String ex = MessageFormat.format("Illegal situation as file ({0}) exists: {1}", file, msg); //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } + } - @Override - public void assertNotExists(String msg, File file) { - if (file.exists()) { - String ex = MessageFormat.format("Illegal situation as file ({0}) exists: {1}", file, msg); //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } + public void assertExists(String msg, File file) { + if (!file.exists()) { + String ex = MessageFormat.format("Illegal situation as file ({0}) does not exist: {1}", file, msg); //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); } - - @Override - public void assertExists(String msg, File file) { - if (!file.exists()) { - String ex = MessageFormat.format("Illegal situation as file ({0}) does not exist: {1}", file, msg); //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); - throw new DbcException(ex); - } - } - }; - - public abstract void assertEquals(String msg, Object value1, Object value2); - - public abstract void assertTrue(String msg, boolean value); - - public abstract void assertFalse(String msg, boolean value); - - public abstract void assertNotEmpty(String msg, String value); - - public abstract void assertNotNull(String msg, Object value); - - public abstract void assertNull(String msg, Object value); - - public abstract void assertNotExists(String msg, File file); - - public abstract void assertExists(String msg, File file); + } public class DbcException extends RuntimeException { private static final long serialVersionUID = 1L; From 5dab5a23ffd0e4b95222a6b01938ad4f2340a2c4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 15 Feb 2014 19:31:47 +0100 Subject: [PATCH 083/180] [Minor] added StringHelper.DOT --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 34b3416f1..61cc47aca 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -40,6 +40,7 @@ public class StringHelper { 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$ From a4c82e6704cd14804e578fd1d1477761a925c489 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 24 Feb 2014 21:45:50 +0100 Subject: [PATCH 084/180] [New] added a DBC.PRE.assertNotEquals() --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 12 +++ .../java/ch/eitchnet/utils/dbc/DBCTest.java | 86 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 8aca39191..ac9b08313 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -42,6 +42,18 @@ public enum DBC { throw new DbcException(ex); } + public void assertNotEquals(String msg, Object value1, Object value2) { + if (value1 != null && !value1.equals(value2)) + return; + + if (value2 != null && !value2.equals(value1)) + return; + + String ex = "Values are equal: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + public void assertTrue(String msg, boolean value) { if (!value) { String ex = "Expected true, but was false: {0}"; //$NON-NLS-1$ diff --git a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java index f0ed48526..2f73cdb45 100644 --- a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java +++ b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java @@ -125,6 +125,92 @@ public class DBCTest { // at ch.eitchnet.utils.DBC.PRE.DBC.PRE.assertEquals(DBC.PRE.java:39) } + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEquals_1() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Values are equal:"); + + String msg = ""; + Object value1 = null; + Object value2 = null; + + DBC.PRE.assertNotEquals(msg, value1, value2); + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEquals_2() throws Exception { + String msg = ""; + Object value1 = new Object(); + Object value2 = new Object(); + + DBC.PRE.assertNotEquals(msg, value1, value2); + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEquals_3() throws Exception { + String msg = ""; + Object value1 = null; + Object value2 = new Object(); + + DBC.PRE.assertNotEquals(msg, value1, value2); + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEquals_4() throws Exception { + String msg = ""; + Object value1 = new Object(); + Object value2 = null; + + DBC.PRE.assertNotEquals(msg, value1, value2); + } + + /** + * Run the void assertEquals(String,Object,Object) method test. + * + * @throws Exception + * + * @generatedBy CodePro at 2/2/14 8:13 PM + */ + @Test + public void testAssertNotEquals_5() throws Exception { + this.exception.expect(DbcException.class); + this.exception.expectMessage("Values are equal:"); + + String msg = ""; + Object value1 = "bla"; + Object value2 = "bla"; + + DBC.PRE.assertNotEquals(msg, value1, value2); + } + /** * Run the void assertExists(String,File) method test. * From 127ab93ea74de9fdaf6e3b1d99a9ffd815f59495 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 25 Feb 2014 21:27:53 +0100 Subject: [PATCH 085/180] [Minor] changed DBC assertNotEquals and assertEquals to take args T --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index ac9b08313..a12cf85f3 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -27,7 +27,7 @@ public enum DBC { PRE, INTERIM, POST; - public void assertEquals(String msg, Object value1, Object value2) { + public void assertEquals(String msg, T value1, T value2) { if (value1 == null && value2 == null) return; @@ -42,7 +42,7 @@ public enum DBC { throw new DbcException(ex); } - public void assertNotEquals(String msg, Object value1, Object value2) { + public void assertNotEquals(String msg, T value1, T value2) { if (value1 != null && !value1.equals(value2)) return; From 6d15ea715bff24e535bd191e5cf18ba65e90e6db Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 14 Mar 2014 14:36:12 +0100 Subject: [PATCH 086/180] [Project] fixed urls of projects --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 143e056af..609b01d1a 100644 --- a/pom.xml +++ b/pom.xml @@ -14,18 +14,18 @@ 0.2.0-SNAPSHOT ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse - https://github.com/eitch/ch.eitchnet.utils + https://github.com/eitchnet/ch.eitchnet.utils 2011 Github Issues - https://github.com/eitch/ch.eitchnet.utils/issues + https://github.com/eitchnet/ch.eitchnet.utils/issues - scm:git:https://github.com/eitch/ch.eitchnet.utils.git - scm:git:git@github.com:eitch/ch.eitchnet.utils.git - https://github.com/eitch/ch.eitchnet.utils + 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 From 58f57f36586655468aebcbf1080fb7e72203563d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 27 Mar 2014 18:49:54 +0100 Subject: [PATCH 087/180] [Minor] StringHelper.formatNanos() now understands up to hours --- .../eitchnet/utils/helper/StringHelper.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 61cc47aca..1999200f6 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -523,36 +523,36 @@ public class StringHelper { } /** - * Formats the given number of milliseconds to a time like 0.000s/ms + * Formats the given number of milliseconds to a time like 0.000h/m/s/ms/us/ns * * @param millis * the number of milliseconds * - * @return format the given number of milliseconds to a time like 0.000s/ms + * @return format the given number of milliseconds to a time like 0.000h/m/s/ms/us/ns */ public static String formatMillisecondsDuration(final long millis) { - if (millis > 1000) { - return String.format("%.3fs", (((double) millis) / 1000)); //$NON-NLS-1$ - } - - return millis + "ms"; //$NON-NLS-1$ + return formatNanoDuration(millis * 1000000L); } /** - * Formats the given number of nanoseconds to a time like 0.000s/ms/us/ns + * Formats the given number of nanoseconds to a time like 0.000h/m/s/ms/us/ns * * @param nanos * the number of nanoseconds * - * @return format the given number of nanoseconds to a time like 0.000s/ms/us/ns + * @return format the given number of nanoseconds to a time like 0.000h/m/s/ms/us/ns */ public static String formatNanoDuration(final long nanos) { - if (nanos > 1000000000) { - return String.format("%.3fs", (((double) nanos) / 1000000000)); //$NON-NLS-1$ - } else if (nanos > 1000000) { - return String.format("%.3fms", (((double) nanos) / 1000000)); //$NON-NLS-1$ - } else if (nanos > 1000) { - return String.format("%.3fus", (((double) nanos) / 1000)); //$NON-NLS-1$ + if (nanos > 3600000000000L) { + return String.format("%.3fh", (nanos / 3600000000000.0D)); //$NON-NLS-1$ + } else if (nanos > 60000000000L) { + return String.format("%.3fm", (nanos / 60000000000.0D)); //$NON-NLS-1$ + } else if (nanos > 1000000000L) { + return String.format("%.3fs", (nanos / 1000000000.0D)); //$NON-NLS-1$ + } else if (nanos > 1000000L) { + return String.format("%.3fms", (nanos / 1000000.0D)); //$NON-NLS-1$ + } else if (nanos > 1000L) { + return String.format("%.3fus", (nanos / 1000.0D)); //$NON-NLS-1$ } else { return nanos + "ns"; //$NON-NLS-1$ } From 468c79baf057dbd4968d5c941fdee07c79f22221 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 28 Mar 2014 15:22:52 +0100 Subject: [PATCH 088/180] [Minor] StringHelper.formatNanos() does not use decimals anymore --- .../eitchnet/utils/helper/StringHelper.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 1999200f6..b7f1b6302 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -523,36 +523,36 @@ public class StringHelper { } /** - * Formats the given number of milliseconds to a time like 0.000h/m/s/ms/us/ns + * 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 0.000h/m/s/ms/us/ns + * @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 0.000h/m/s/ms/us/ns + * 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 0.000h/m/s/ms/us/ns + * @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("%.3fh", (nanos / 3600000000000.0D)); //$NON-NLS-1$ - } else if (nanos > 60000000000L) { - return String.format("%.3fm", (nanos / 60000000000.0D)); //$NON-NLS-1$ - } else if (nanos > 1000000000L) { - return String.format("%.3fs", (nanos / 1000000000.0D)); //$NON-NLS-1$ - } else if (nanos > 1000000L) { - return String.format("%.3fms", (nanos / 1000000.0D)); //$NON-NLS-1$ - } else if (nanos > 1000L) { - return String.format("%.3fus", (nanos / 1000.0D)); //$NON-NLS-1$ + 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$ } From 1b1eb982c2ce3db61e9b1363f6487593cec7163a Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 28 Mar 2014 19:05:34 +0100 Subject: [PATCH 089/180] [Minor] Added MapOfMaps.size() and .size(T) methods --- .../ch/eitchnet/utils/collections/MapOfMaps.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java index 4a6f6ed15..887999eaf 100644 --- a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java @@ -125,4 +125,18 @@ public class MapOfMaps { return false; return map.containsKey(u); } + + public int size() { + int size = 0; + Set>> entrySet = this.mapOfMaps.entrySet(); + Iterator>> iter = entrySet.iterator(); + while (iter.hasNext()) { + size += iter.next().getValue().size(); + } + return size; + } + + public int size(T t) { + return this.mapOfMaps.get(t).size(); + } } From cfb9e3e0ba3bb6771b7835f889dbd1fe1f3b441e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 4 Aug 2014 12:45:47 +0200 Subject: [PATCH 090/180] [New] added DBC.assertNotEmpty() methods for arrays and collections --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index a12cf85f3..21d2d2755 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -17,6 +17,7 @@ package ch.eitchnet.utils.dbc; import java.io.File; import java.text.MessageFormat; +import java.util.Collection; import ch.eitchnet.utils.helper.StringHelper; @@ -78,6 +79,24 @@ public enum DBC { } } + public void assertNotEmpty(String msg, Object[] array) { + assertNotNull(msg, array); + if (array.length == 0) { + String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + public void assertNotEmpty(String msg, Collection collection) { + assertNotNull(msg, collection); + if (collection.isEmpty()) { + String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + public void assertNotNull(String msg, Object value) { if (value == null) { String ex = "Illegal null value: {0}"; //$NON-NLS-1$ From 2a951e41ceea757ba5e157c9788971fd87f04511 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 4 Aug 2014 17:53:54 +0200 Subject: [PATCH 091/180] [Minor] fixed formatting of SystemHelper.asString() --- src/main/java/ch/eitchnet/utils/helper/SystemHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java index 3b72d38d1..f7f97d8f0 100644 --- a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java @@ -59,11 +59,11 @@ public class SystemHelper { public static String asString() { StringBuilder sb = new StringBuilder(); sb.append(SystemHelper.osName); - sb.append(StringHelper.EMPTY); + sb.append(StringHelper.SPACE); sb.append(SystemHelper.osArch); - sb.append(StringHelper.EMPTY); + sb.append(StringHelper.SPACE); sb.append(SystemHelper.osVersion); - sb.append(StringHelper.EMPTY); + sb.append(StringHelper.SPACE); sb.append("on Java "); //$NON-NLS-1$ sb.append(SystemHelper.javaVendor); sb.append(" version "); //$NON-NLS-1$ From 2a05432152c99b37bc12550e3ee55c03533f0e62 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 7 Aug 2014 00:42:36 +0200 Subject: [PATCH 092/180] [New] Added DBC.assertEmpty()-methods --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 21d2d2755..68d1e1928 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -71,6 +71,32 @@ public enum DBC { } } + public void assertEmpty(String msg, String value) { + if (!StringHelper.isEmpty(value)) { + String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + public void assertEmpty(String msg, Object[] array) { + assertNotNull(msg, array); + if (array.length != 0) { + String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + + public void assertEmpty(String msg, Collection collection) { + assertNotNull(msg, collection); + if (!collection.isEmpty()) { + String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg); + throw new DbcException(ex); + } + } + public void assertNotEmpty(String msg, String value) { if (StringHelper.isEmpty(value)) { String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ From 383de8c8710c843f1e2afa3f9686989d0c84fa2f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 8 Aug 2014 11:09:41 +0200 Subject: [PATCH 093/180] [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package [Devel] added communication package --- .../ch/eitchnet/communication/CommandKey.java | 68 +++ .../CommunicationConnection.java | 360 +++++++++++ .../communication/CommunicationEndpoint.java | 36 ++ .../communication/ConnectionException.java | 18 + .../communication/ConnectionMessages.java | 147 +++++ .../communication/ConnectionMode.java | 40 ++ .../communication/ConnectionObserver.java | 24 + .../communication/ConnectionState.java | 39 ++ .../ch/eitchnet/communication/IoMessage.java | 99 +++ .../communication/IoMessageVisitor.java | 25 + .../communication/StreamMessageVisitor.java | 22 + .../console/ConsoleEndpoint.java | 63 ++ .../console/ConsoleMessageVisitor.java | 11 + .../communication/file/FileEndpoint.java | 242 ++++++++ .../communication/file/FileEndpointMode.java | 20 + .../tcpip/ClientSocketEndpoint.java | 510 ++++++++++++++++ .../tcpip/ServerSocketEndpoint.java | 571 ++++++++++++++++++ .../tcpip/SocketEndpointConstants.java | 67 ++ .../tcpip/SocketMessageVisitor.java | 15 + .../utils/collections/MapOfLists.java | 120 ++++ .../eitchnet/utils/collections/MapOfMaps.java | 9 +- .../communication/AbstractEndpointTest.java | 40 ++ .../communication/ConsoleEndpointTest.java | 61 ++ .../communication/FileEndpointTest.java | 120 ++++ .../communication/SocketEndpointTest.java | 134 ++++ .../communication/TestConnectionObserver.java | 23 + .../eitchnet/communication/TestIoMessage.java | 25 + 27 files changed, 2908 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ch/eitchnet/communication/CommandKey.java create mode 100644 src/main/java/ch/eitchnet/communication/CommunicationConnection.java create mode 100644 src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionException.java create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionMessages.java create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionMode.java create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionObserver.java create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionState.java create mode 100644 src/main/java/ch/eitchnet/communication/IoMessage.java create mode 100644 src/main/java/ch/eitchnet/communication/IoMessageVisitor.java create mode 100644 src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java create mode 100644 src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java create mode 100644 src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java create mode 100644 src/main/java/ch/eitchnet/communication/file/FileEndpoint.java create mode 100644 src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java create mode 100644 src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java create mode 100644 src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java create mode 100644 src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java create mode 100644 src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java create mode 100644 src/main/java/ch/eitchnet/utils/collections/MapOfLists.java create mode 100644 src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java create mode 100644 src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java create mode 100644 src/test/java/ch/eitchnet/communication/FileEndpointTest.java create mode 100644 src/test/java/ch/eitchnet/communication/SocketEndpointTest.java create mode 100644 src/test/java/ch/eitchnet/communication/TestConnectionObserver.java create mode 100644 src/test/java/ch/eitchnet/communication/TestIoMessage.java diff --git a/src/main/java/ch/eitchnet/communication/CommandKey.java b/src/main/java/ch/eitchnet/communication/CommandKey.java new file mode 100644 index 000000000..b65ee5aa6 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/CommandKey.java @@ -0,0 +1,68 @@ +/* + * 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.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/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java new file mode 100644 index 000000000..49faf5d44 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -0,0 +1,360 @@ +/* + * 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 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.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 CommunicationEndpoint endpoint; + private IoMessageVisitor converter; + + public CommunicationConnection(String id, ConnectionMode mode, Map parameters, + CommunicationEndpoint endpoint, IoMessageVisitor converter) { + this.id = id; + this.mode = mode; + this.parameters = parameters; + this.endpoint = endpoint; + this.converter = converter; + + this.state = ConnectionState.CREATED; + this.stateMsg = this.state.toString(); + this.messageQueue = new LinkedBlockingDeque<>(); + this.connectionObservers = new MapOfLists<>(); + } + + 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 notifyStateChange(ConnectionState state, String stateMsg) { + this.state = state; + this.stateMsg = 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.converter.configure(this); + this.endpoint.configure(this, this.converter); + this.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: + logger.info("Connecting..."); //$NON-NLS-1$ + if (this.queueThread != null) { + logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$ + } else { + logger.info(MessageFormat.format("Starting Integration connection {0}...", this.id)); //$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 entpoint when a new message has been received and parsed + * + * @param message + */ + public void notify(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); + + 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())); + } + } + } + + @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$ + + this.endpoint.send(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); + } + } + } + } + + /** + * Called when the message has been handled + * + * @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; + + } + synchronized (this.connectionObservers) { + List observers = this.connectionObservers.getList(message.getKey()); + for (ConnectionObserver observer : observers) { + observer.notify(message.getKey(), message); + } + } + } + + public String getUri() { + return this.endpoint == null ? "0.0.0.0:0" : this.endpoint.getRemoteUri(); //$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()); + + if (this.mode == ConnectionMode.SIMULATION) { + message.setState(State.DONE, State.DONE.name()); + done(message); + } else { + this.messageQueue.add(message); + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java new file mode 100644 index 000000000..7aaf10a44 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * @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; +} diff --git a/src/main/java/ch/eitchnet/communication/ConnectionException.java b/src/main/java/ch/eitchnet/communication/ConnectionException.java new file mode 100644 index 000000000..a6e75de2b --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionException.java @@ -0,0 +1,18 @@ +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/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java new file mode 100644 index 000000000..a6fab0c79 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java @@ -0,0 +1,147 @@ +package ch.eitchnet.communication; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +import org.apache.log4j.Logger; + +import ch.eitchnet.utils.helper.StringHelper; + +/** + * Helper class to thrown connection messages + * + * @author Robert von Burg + */ +public class ConnectionMessages { + + private static Logger logger = Logger.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.getName(), 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.getName(), 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) { + String msg = "{0}: parameter ''{1}'' is not set, using default value ''{2}''"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, clazz.getName(), 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 endpoint, + Class 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/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/src/main/java/ch/eitchnet/communication/ConnectionMode.java new file mode 100644 index 000000000..e08234c86 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionMode.java @@ -0,0 +1,40 @@ +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, + + /** + * 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, + + /** + * 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; +} diff --git a/src/main/java/ch/eitchnet/communication/ConnectionObserver.java b/src/main/java/ch/eitchnet/communication/ConnectionObserver.java new file mode 100644 index 000000000..a0b5adc5e --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionObserver.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * @author Robert von Burg + */ +public interface ConnectionObserver { + + public void notify(CommandKey key, IoMessage message); +} diff --git a/src/main/java/ch/eitchnet/communication/ConnectionState.java b/src/main/java/ch/eitchnet/communication/ConnectionState.java new file mode 100644 index 000000000..49cd31bca --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionState.java @@ -0,0 +1,39 @@ +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/src/main/java/ch/eitchnet/communication/IoMessage.java b/src/main/java/ch/eitchnet/communication/IoMessage.java new file mode 100644 index 000000000..f473b0df3 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/IoMessage.java @@ -0,0 +1,99 @@ +package ch.eitchnet.communication; + +import java.util.Date; + +import ch.eitchnet.utils.helper.StringHelper; +import ch.eitchnet.utils.iso8601.ISO8601FormatFactory; + +public class IoMessage { + + private final String id; + private final CommandKey key; + private Date updated; + private State state; + private String stateMsg; + + public IoMessage(String id, CommandKey key) { + this.id = id; + this.key = key; + this.state = State.CREATED; + this.stateMsg = StringHelper.DASH; + this.updated = new Date(); + } + + /** + * @return the id + */ + public String getId() { + return this.id; + } + + /** + * @return the key + */ + public CommandKey getKey() { + return this.key; + } + + /** + * @return the updated + */ + public Date getUpdated() { + return this.updated; + } + + /** + * @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("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(); + } + + public enum State { + CREATED, // new + PENDING, // outbound + ACCEPTED, // inbound + DONE, // completed + FAILED, // completed + FATAL; // not sendable + } +} diff --git a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java new file mode 100644 index 000000000..f1ae3ac68 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java @@ -0,0 +1,25 @@ +package ch.eitchnet.communication; + +import ch.eitchnet.communication.console.ConsoleMessageVisitor; + +/** + *

+ * Visitors to read and write {@link IoMessage} using different kind of endpoints. Different entpoints 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 CommunicationConnection connection; + + public void configure(CommunicationConnection connection) { + this.connection = connection; + } +} diff --git a/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java b/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java new file mode 100644 index 000000000..052f45b51 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java @@ -0,0 +1,22 @@ +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/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java new file mode 100644 index 000000000..a3f290386 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java @@ -0,0 +1,63 @@ +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()); + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java b/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java new file mode 100644 index 000000000..9991589b4 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java @@ -0,0 +1,11 @@ +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/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java new file mode 100644 index 000000000..3f9d15d80 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2006 - 2011 + * + * Apixxo AG + * Hauptgasse 25 + * 4600 Olten + * + * All rights reserved. + * + */ + +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 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.notify(message); + } + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java b/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java new file mode 100644 index 000000000..798867071 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java @@ -0,0 +1,20 @@ +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/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java new file mode 100644 index 000000000..c96a23b98 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -0,0 +1,510 @@ +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.apache.log4j.Logger; + +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 = Logger.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; + private DataOutputStream outputStream; + private DataInputStream inputStream; + + private CommunicationConnection connection; + + private 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 + 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); + } + + 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}"; //$NON-NLS-1$ + logger.error(MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort))); + logger.error(e, e); + 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}
  • + *
+ * + * @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.warn("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 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 { + logger.error(e, e); + message.setState(State.FATAL, e.getLocalizedMessage()); + this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); + } + } finally { + if (this.closeAfterSend) { + closeConnection(); + } + } + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java new file mode 100644 index 000000000..17bbe3cc9 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -0,0 +1,571 @@ +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.apache.log4j.Logger; + +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 = Logger.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; + private DataOutputStream outputStream; + private DataInputStream inputStream; + + private CommunicationConnection connection; + + private 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 + 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())); + //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}"; //$NON-NLS-1$ + logger.error(MessageFormat.format(msg, this.connection.getId())); + logger.error(e, e); + 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.warn("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.fatal("Fatal BindException occurred! Port is already in use, or address is illegal!"); //$NON-NLS-1$ + logger.fatal(e, 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.notify(message); + } + } + + } catch (Exception e) { + if (e instanceof InterruptedException) { + logger.error("Interrupted!"); //$NON-NLS-1$ + } else { + logger.error(e, 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 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/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java new file mode 100644 index 000000000..ea967644b --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java @@ -0,0 +1,67 @@ +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/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java new file mode 100644 index 000000000..056bb04aa --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java @@ -0,0 +1,15 @@ +package ch.eitchnet.communication.tcpip; + +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import ch.eitchnet.communication.IoMessage; +import ch.eitchnet.communication.IoMessageVisitor; + +public abstract class SocketMessageVisitor extends IoMessageVisitor { + + public abstract IoMessage visit(DataInputStream inputStream, DataOutputStream outputStream) throws Exception; + + public abstract void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message) + throws Exception; +} diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java b/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java new file mode 100644 index 000000000..dc64e2c79 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java @@ -0,0 +1,120 @@ +/* + * 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(); + } +} diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java index 887999eaf..4ea4337fe 100644 --- a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java @@ -126,6 +126,10 @@ public class MapOfMaps { return map.containsKey(u); } + public int sizeKeys() { + return this.mapOfMaps.size(); + } + public int size() { int size = 0; Set>> entrySet = this.mapOfMaps.entrySet(); @@ -137,6 +141,9 @@ public class MapOfMaps { } public int size(T t) { - return this.mapOfMaps.get(t).size(); + Map map = this.mapOfMaps.get(t); + if (map == null) + return 0; + return map.size(); } } diff --git a/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java b/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java new file mode 100644 index 000000000..2d1f65ee7 --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java @@ -0,0 +1,40 @@ +package ch.eitchnet.communication; + +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AbstractEndpointTest { + + static final Logger logger = LoggerFactory.getLogger(FileEndpointTest.class); + + public static TestIoMessage createTestMessage(String key1, String key2) { + return createTestMessage(CommandKey.key(key1, key2)); + } + + @SuppressWarnings("nls") + public static TestIoMessage createTestMessage(CommandKey key) { + TestIoMessage msg = new TestIoMessage(UUID.randomUUID().toString(), key); + List lines = new ArrayList<>(); + lines.add("bla"); + lines.add("foo"); + lines.add("bar"); + lines.add("bla"); + msg.setContents(lines); + return msg; + } + + protected void waitForMessage(TestConnectionObserver observer) throws InterruptedException { + long start = System.currentTimeMillis(); + while (observer.getMessage() == null) { + if (System.currentTimeMillis() - start > 2000) + fail("Connection didn't send message in 2s!"); //$NON-NLS-1$ + Thread.sleep(50); + } + } +} diff --git a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java new file mode 100644 index 000000000..54d4f960e --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java @@ -0,0 +1,61 @@ +package ch.eitchnet.communication; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; + +import ch.eitchnet.communication.console.ConsoleEndpoint; +import ch.eitchnet.communication.console.ConsoleMessageVisitor; + +public class ConsoleEndpointTest extends AbstractEndpointTest { + + private static final String CONNECTION_ID = "Console"; //$NON-NLS-1$ + private CommunicationConnection connection; + + @Before + public void before() { + + Map parameters = new HashMap<>(); + CommunicationEndpoint endpoint = new ConsoleEndpoint(); + ConsoleMessageVisitor messageVisitor = new ConsoleMessageVisitorExtension(); + this.connection = new CommunicationConnection(CONNECTION_ID, ConnectionMode.ON, parameters, endpoint, + messageVisitor); + this.connection.configure(); + } + + @Test + public void testConsoleEndpoint() throws InterruptedException { + + this.connection.start(); + + CommandKey key = CommandKey.key(CONNECTION_ID, "logger"); //$NON-NLS-1$ + TestIoMessage msg = createTestMessage(key); + + TestConnectionObserver observer = new TestConnectionObserver(); + this.connection.addConnectionObserver(key, observer); + this.connection.send(msg); + waitForMessage(observer); + + assertEquals(msg.getKey(), observer.getMessage().getKey()); + + } + + private final class ConsoleMessageVisitorExtension extends ConsoleMessageVisitor { + public ConsoleMessageVisitorExtension() { + // no-op + } + + @Override + public void visit(Logger logger, IoMessage message) throws Exception { + TestIoMessage msg = (TestIoMessage) message; + for (String line : msg.getContents()) { + logger.info(line); + } + } + } +} diff --git a/src/test/java/ch/eitchnet/communication/FileEndpointTest.java b/src/test/java/ch/eitchnet/communication/FileEndpointTest.java new file mode 100644 index 000000000..454433a76 --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/FileEndpointTest.java @@ -0,0 +1,120 @@ +package ch.eitchnet.communication; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import ch.eitchnet.communication.file.FileEndpoint; +import ch.eitchnet.communication.file.FileEndpointMode; +import ch.eitchnet.utils.helper.FileHelper; + +public class FileEndpointTest extends AbstractEndpointTest { + + public static final String INBOUND_FILENAME = "target/test_in.txt"; //$NON-NLS-1$ + public static final String OUTBOUND_FILENAME = "target/test_out.txt"; //$NON-NLS-1$ + public static final String CONNECTION_ID = "FileTestEndpoint"; //$NON-NLS-1$ + + private CommunicationConnection connection; + + @Before + public void before() { + + new File(OUTBOUND_FILENAME).delete(); + new File(INBOUND_FILENAME).delete(); + + Map parameters = new HashMap<>(); + parameters.put(FileEndpoint.ENDPOINT_MODE, FileEndpointMode.READ_WRITE.name()); + parameters.put(FileEndpoint.INBOUND_FILENAME, INBOUND_FILENAME); + parameters.put(FileEndpoint.OUTBOUND_FILENAME, OUTBOUND_FILENAME); + + ConnectionMode mode = ConnectionMode.ON; + CommunicationEndpoint endpoint = new FileEndpoint(); + StreamMessageVisitor messageVisitor = new StreamMessageVisitorExtension(); + + this.connection = new CommunicationConnection(CONNECTION_ID, mode, parameters, endpoint, messageVisitor); + this.connection.configure(); + } + + @After + public void after() { + if (this.connection != null) + this.connection.stop(); + } + + @Test + public void testFileEndpoint() throws InterruptedException { + + String inboundFilename = new File(INBOUND_FILENAME).getName(); + String outboundFilename = new File(OUTBOUND_FILENAME).getName(); + + // send a message + this.connection.start(); + TestConnectionObserver outboundObserver = new TestConnectionObserver(); + TestIoMessage message = createTestMessage(outboundFilename, FileEndpointMode.WRITE.name()); + this.connection.addConnectionObserver(message.getKey(), outboundObserver); + this.connection.send(message); + + // wait till the message has been sent + waitForMessage(outboundObserver); + this.connection.stop(); + assertEquals(message.getKey(), outboundObserver.getMessage().getKey()); + + // now test reading a file + this.connection.start(); + CommandKey inboundKey = CommandKey.key(inboundFilename, FileEndpointMode.READ.name()); + TestConnectionObserver inboundObserver = new TestConnectionObserver(); + this.connection.addConnectionObserver(inboundKey, inboundObserver); + FileHelper.writeStringToFile("Hello\nWorld!", new File(INBOUND_FILENAME)); //$NON-NLS-1$ + + // wait for thread to pick up the file + waitForMessage(inboundObserver); + assertEquals(inboundKey, inboundObserver.getMessage().getKey()); + } + + public static final class StreamMessageVisitorExtension extends StreamMessageVisitor { + private String inboundFilename; + + @Override + public void configure(CommunicationConnection connection) { + super.configure(connection); + Map parameters = connection.getParameters(); + String filePath = parameters.get(FileEndpoint.INBOUND_FILENAME); + this.inboundFilename = new File(filePath).getName(); + } + + @Override + public IoMessage visit(InputStream inputStream) throws Exception { + + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + List lines = new ArrayList<>(); + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + return new TestIoMessage(UUID.randomUUID().toString(), CommandKey.key(this.inboundFilename, + FileEndpointMode.READ.name()), lines); + } + + @Override + public void visit(OutputStream outputStream, IoMessage message) throws Exception { + TestIoMessage msg = (TestIoMessage) message; + for (String line : msg.getContents()) { + outputStream.write(line.getBytes()); + outputStream.write('\n'); + } + } + } +} diff --git a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java new file mode 100644 index 000000000..80227ac0a --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java @@ -0,0 +1,134 @@ +package ch.eitchnet.communication; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import ch.eitchnet.communication.tcpip.ClientSocketEndpoint; +import ch.eitchnet.communication.tcpip.ServerSocketEndpoint; +import ch.eitchnet.communication.tcpip.SocketEndpointConstants; +import ch.eitchnet.communication.tcpip.SocketMessageVisitor; + +public class SocketEndpointTest extends AbstractEndpointTest { + + private static final String PORT = "45678"; //$NON-NLS-1$ + private static final String HOST = "localhost"; //$NON-NLS-1$ + private static final String CLIENT_CONNECTION_ID = "ClientSocket"; //$NON-NLS-1$ + private static final String SERVER_CONNECTION_ID = "ServerSocket"; //$NON-NLS-1$ + private CommunicationConnection clientConnection; + private CommunicationConnection serverConnection; + + @Before + public void before() { + + { + Map parameters = new HashMap<>(); + parameters.put(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_ADDRESS, HOST); + parameters.put(SocketEndpointConstants.PARAMETER_REMOTE_INPUT_PORT, PORT); + + // we close after send, so that the server can read whole lines, as that is what we are sending + parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.TRUE.toString()); + + CommunicationEndpoint endpoint = new ClientSocketEndpoint(); + SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(); + this.clientConnection = new CommunicationConnection(CLIENT_CONNECTION_ID, ConnectionMode.ON, parameters, + endpoint, messageVisitor); + this.clientConnection.configure(); + } + + { + Map parameters = new HashMap<>(); + parameters.put(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_ADDRESS, HOST); + parameters.put(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT, PORT); + + CommunicationEndpoint endpoint = new ServerSocketEndpoint(); + SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(); + this.serverConnection = new CommunicationConnection(SERVER_CONNECTION_ID, ConnectionMode.ON, parameters, + endpoint, messageVisitor); + this.serverConnection.configure(); + } + } + + @After + public void after() { + if (this.clientConnection != null) + this.clientConnection.stop(); + if (this.serverConnection != null) + this.serverConnection.stop(); + } + + @Test + public void testSocketEndpoints() throws Exception { + + this.serverConnection.start(); + Thread.sleep(100); + this.clientConnection.start(); + + TestConnectionObserver serverObserver = new TestConnectionObserver(); + CommandKey inboundKey = CommandKey.key(SERVER_CONNECTION_ID, "lines"); //$NON-NLS-1$ + this.serverConnection.addConnectionObserver(inboundKey, serverObserver); + + TestConnectionObserver clientObserver = new TestConnectionObserver(); + CommandKey outboundKey = CommandKey.key(CLIENT_CONNECTION_ID, "lines"); //$NON-NLS-1$ + this.clientConnection.addConnectionObserver(outboundKey, clientObserver); + + TestIoMessage outboundMsg = createTestMessage(outboundKey); + this.clientConnection.send(outboundMsg); + waitForMessage(clientObserver); + assertEquals(outboundMsg.getKey(), clientObserver.getMessage().getKey()); + + waitForMessage(serverObserver); + assertEquals(inboundKey, serverObserver.getMessage().getKey()); + assertEquals(outboundMsg.getContents(), ((TestIoMessage) serverObserver.getMessage()).getContents()); + } + + private final class SocketMessageVisitorExtension extends SocketMessageVisitor { + public SocketMessageVisitorExtension() { + // do nothing + } + + @Override + public void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message) + throws Exception { + TestIoMessage msg = (TestIoMessage) message; + logger.info(MessageFormat + .format("Writing {0} lines for message {1}", msg.getContents().size(), msg.getId())); //$NON-NLS-1$ + for (String line : msg.getContents()) { + outputStream.writeBytes(line); + outputStream.write('\n'); + } + outputStream.flush(); + } + + @Override + public IoMessage visit(DataInputStream inputStream, DataOutputStream outputStream) throws Exception { + + List lines = new ArrayList<>(); + + // since we are reading whole lines, we must close the stream when we read null i.e. EOF + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + logger.info("Reading from stream..."); //$NON-NLS-1$ + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + logger.info(MessageFormat.format("Read {0} lines from stream.", lines.size())); //$NON-NLS-1$ + + return new TestIoMessage(UUID.randomUUID().toString(), CommandKey.key(SERVER_CONNECTION_ID, "lines"), lines); //$NON-NLS-1$ + } + } +} diff --git a/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java b/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java new file mode 100644 index 000000000..a2882b7cd --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java @@ -0,0 +1,23 @@ +package ch.eitchnet.communication; + +import java.text.MessageFormat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestConnectionObserver implements ConnectionObserver { + + private static final Logger logger = LoggerFactory.getLogger(FileEndpointTest.class); + + private IoMessage message; + + public IoMessage getMessage() { + return this.message; + } + + @Override + public void notify(CommandKey key, IoMessage message) { + this.message = message; + logger.info(MessageFormat.format("Received message with key {0} and message {1}", key, message)); //$NON-NLS-1$ + } +} \ No newline at end of file diff --git a/src/test/java/ch/eitchnet/communication/TestIoMessage.java b/src/test/java/ch/eitchnet/communication/TestIoMessage.java new file mode 100644 index 000000000..cdd3d139b --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/TestIoMessage.java @@ -0,0 +1,25 @@ +package ch.eitchnet.communication; + +import java.util.List; + +public class TestIoMessage extends IoMessage { + + private List contents; + + public TestIoMessage(String id, CommandKey key) { + super(id, key); + } + + public TestIoMessage(String id, CommandKey key, List contents) { + super(id, key); + this.contents = contents; + } + + public List getContents() { + return this.contents; + } + + public void setContents(List contents) { + this.contents = contents; + } +} From c0655417dad6307eaec4d0a2674a3a94be9d2165 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 14:21:11 +0200 Subject: [PATCH 094/180] [New] added MapOfMaps.getAllElements() methods --- .../eitchnet/utils/collections/MapOfMaps.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java index 4ea4337fe..7e829ee0b 100644 --- a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java @@ -15,8 +15,10 @@ */ 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; @@ -80,6 +82,23 @@ public class MapOfMaps { return map.put(u, v); } + public List getAllElements() { + List all = new ArrayList<>(); + for (Map u : this.mapOfMaps.values()) { + all.addAll(u.values()); + } + return all; + } + + public List getAllElements(T t) { + List all = new ArrayList<>(); + Map map = this.mapOfMaps.get(t); + if (map != null) { + all.addAll(map.values()); + } + return all; + } + public void addMap(T t, Map u) { Map map = this.mapOfMaps.get(t); if (map == null) { From 22ddf8db7ec0bc45bb94af7410041f733719f1a6 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 14:27:38 +0200 Subject: [PATCH 095/180] [Devel] added communication package [Devel] added communication package --- .../ch/eitchnet/communication/CommandKey.java | 2 +- .../CommunicationConnection.java | 71 ++++++---- .../communication/CommunicationEndpoint.java | 2 +- .../communication/ConnectionException.java | 15 +++ .../communication/ConnectionMessages.java | 15 +++ .../communication/ConnectionMode.java | 15 +++ .../communication/ConnectionObserver.java | 2 +- .../communication/ConnectionState.java | 15 +++ .../ch/eitchnet/communication/IoMessage.java | 60 ++++++++- .../communication/IoMessageArchive.java | 41 ++++++ .../communication/IoMessageVisitor.java | 15 +++ .../communication/SimpleMessageArchive.java | 121 ++++++++++++++++++ .../communication/StreamMessageVisitor.java | 15 +++ .../console/ConsoleEndpoint.java | 15 +++ .../console/ConsoleMessageVisitor.java | 15 +++ .../communication/file/FileEndpoint.java | 16 ++- .../communication/file/FileEndpointMode.java | 15 +++ .../tcpip/ClientSocketEndpoint.java | 15 +++ .../tcpip/ServerSocketEndpoint.java | 15 +++ .../tcpip/SocketEndpointConstants.java | 15 +++ .../tcpip/SocketMessageVisitor.java | 15 +++ .../utils/exceptions/XmlException.java | 1 - .../eitchnet/utils/helper/ArraysHelper.java | 1 - .../ch/eitchnet/utils/helper/ByteHelper.java | 1 - .../ch/eitchnet/utils/helper/MathHelper.java | 3 +- .../utils/io/FileProgressListener.java | 17 ++- .../utils/io/FileStreamProgressWatcher.java | 15 +++ .../utils/io/LoggingFileProgressListener.java | 15 +++ .../utils/io/ProgressableFileInputStream.java | 15 +++ .../ch/eitchnet/utils/iso8601/DateFormat.java | 4 +- .../utils/iso8601/DurationFormat.java | 4 +- .../eitchnet/utils/iso8601/FormatFactory.java | 12 +- .../ch/eitchnet/utils/iso8601/ISO8601.java | 2 +- .../utils/iso8601/ISO8601Duration.java | 4 +- .../utils/iso8601/ISO8601FormatFactory.java | 2 +- .../utils/iso8601/ISO8601Worktime.java | 4 +- .../utils/iso8601/WorktimeFormat.java | 2 +- .../communication/AbstractEndpointTest.java | 38 +++++- .../communication/ConsoleEndpointTest.java | 20 ++- .../communication/FileEndpointTest.java | 22 +++- .../SimpleMessageArchiveTest.java | 70 ++++++++++ .../communication/SocketEndpointTest.java | 23 +++- .../communication/TestConnectionObserver.java | 18 +++ .../eitchnet/communication/TestIoMessage.java | 26 +++- .../java/ch/eitchnet/utils/dbc/DBCTest.java | 17 ++- 45 files changed, 774 insertions(+), 72 deletions(-) create mode 100644 src/main/java/ch/eitchnet/communication/IoMessageArchive.java create mode 100644 src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java create mode 100644 src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java diff --git a/src/main/java/ch/eitchnet/communication/CommandKey.java b/src/main/java/ch/eitchnet/communication/CommandKey.java index b65ee5aa6..f34389bf0 100644 --- a/src/main/java/ch/eitchnet/communication/CommandKey.java +++ b/src/main/java/ch/eitchnet/communication/CommandKey.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Robert von Burg + * 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. diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 49faf5d44..803dffc58 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -1,23 +1,17 @@ /* - * 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 - * . + * 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; @@ -33,6 +27,7 @@ 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; /** @@ -55,15 +50,24 @@ public class CommunicationConnection implements Runnable { private MapOfLists connectionObservers; private CommunicationEndpoint endpoint; - private IoMessageVisitor converter; + private IoMessageVisitor messageVisitor; + + private IoMessageArchive archive; public CommunicationConnection(String id, ConnectionMode mode, Map parameters, - CommunicationEndpoint endpoint, IoMessageVisitor converter) { + 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.converter = converter; + this.messageVisitor = messageVisitor; this.state = ConnectionState.CREATED; this.stateMsg = this.state.toString(); @@ -71,6 +75,14 @@ public class CommunicationConnection implements Runnable { this.connectionObservers = new MapOfLists<>(); } + public void setArchive(IoMessageArchive archive) { + this.archive = archive; + } + + public IoMessageArchive getArchive() { + return this.archive; + } + public String getId() { return this.id; } @@ -132,8 +144,8 @@ public class CommunicationConnection implements Runnable { * Configure the underlying {@link CommunicationEndpoint} and {@link IoMessageVisitor} */ public void configure() { - this.converter.configure(this); - this.endpoint.configure(this, this.converter); + this.messageVisitor.configure(this); + this.endpoint.configure(this, this.messageVisitor); this.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.name()); } @@ -152,7 +164,7 @@ public class CommunicationConnection implements Runnable { if (this.queueThread != null) { logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$ } else { - logger.info(MessageFormat.format("Starting Integration connection {0}...", this.id)); //$NON-NLS-1$ + logger.info(MessageFormat.format("Starting Connection {0}...", this.id)); //$NON-NLS-1$ this.run = true; this.queueThread = new Thread(this, MessageFormat.format("{0}_OUT", this.id)); //$NON-NLS-1$ this.queueThread.start(); @@ -207,7 +219,7 @@ public class CommunicationConnection implements Runnable { } /** - * Called by the underlying entpoint when a new message has been received and parsed + * Called by the underlying endpoint when a new message has been received and parsed * * @param message */ @@ -237,6 +249,9 @@ public class CommunicationConnection implements Runnable { logger.error(MessageFormat.format(msg, message.getKey(), message.getId())); } } + + if (this.archive != null) + this.archive.archive(message); } @Override @@ -278,6 +293,10 @@ public class CommunicationConnection implements Runnable { message.setState(State.FATAL, e.getLocalizedMessage()); done(message); } + } finally { + if (message != null && this.archive != null) { + this.archive.archive(message); + } } } } diff --git a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java index 7aaf10a44..ef0a75813 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Robert von Burg + * 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. diff --git a/src/main/java/ch/eitchnet/communication/ConnectionException.java b/src/main/java/ch/eitchnet/communication/ConnectionException.java index a6e75de2b..465477567 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionException.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionException.java @@ -1,3 +1,18 @@ +/* + * 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; /** diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java index a6fab0c79..d3f4940b4 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/src/main/java/ch/eitchnet/communication/ConnectionMode.java index e08234c86..fbc44a768 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMode.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMode.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/ConnectionObserver.java b/src/main/java/ch/eitchnet/communication/ConnectionObserver.java index a0b5adc5e..8445ca357 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionObserver.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionObserver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Robert von Burg + * 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. diff --git a/src/main/java/ch/eitchnet/communication/ConnectionState.java b/src/main/java/ch/eitchnet/communication/ConnectionState.java index 49cd31bca..13724331e 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionState.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionState.java @@ -1,3 +1,18 @@ +/* + * 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; /** diff --git a/src/main/java/ch/eitchnet/communication/IoMessage.java b/src/main/java/ch/eitchnet/communication/IoMessage.java index f473b0df3..554a8d144 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessage.java +++ b/src/main/java/ch/eitchnet/communication/IoMessage.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -9,13 +24,15 @@ public class IoMessage { private final String id; private final CommandKey key; + private final String connectionId; private Date updated; private State state; private String stateMsg; - public IoMessage(String id, CommandKey key) { + 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(); @@ -35,6 +52,13 @@ public class IoMessage { return this.key; } + /** + * @return the connectionId + */ + public String getConnectionId() { + return this.connectionId; + } + /** * @return the updated */ @@ -42,6 +66,15 @@ public class IoMessage { return this.updated; } + /** + * Used for testing purposes only! + * + * @param date + */ + void setUpdated(Date date) { + this.updated = date; + } + /** * @return the state */ @@ -88,6 +121,31 @@ public class IoMessage { 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 diff --git a/src/main/java/ch/eitchnet/communication/IoMessageArchive.java b/src/main/java/ch/eitchnet/communication/IoMessageArchive.java new file mode 100644 index 000000000..ee0af8077 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java index f1ae3ac68..c7505ce88 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java @@ -1,3 +1,18 @@ +/* + * 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.communication.console.ConsoleMessageVisitor; diff --git a/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java b/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java new file mode 100644 index 000000000..27414f3db --- /dev/null +++ b/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/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java b/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java index 052f45b51..894ebf883 100644 --- a/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java index a3f290386..11bdd4e3d 100644 --- a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java b/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java index 9991589b4..ccad0ef30 100644 --- a/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java index 3f9d15d80..a320a10d7 100644 --- a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java @@ -1,14 +1,18 @@ /* - * Copyright (c) 2006 - 2011 + * Copyright 2014 Robert von Burg * - * Apixxo AG - * Hauptgasse 25 - * 4600 Olten + * 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 * - * All rights reserved. + * 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; diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java b/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java index 798867071..842b0b00e 100644 --- a/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java @@ -1,3 +1,18 @@ +/* + * 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 { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index c96a23b98..99052e43d 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 17bbe3cc9..4619c0774 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java index ea967644b..74ccbdc0a 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java @@ -1,3 +1,18 @@ +/* + * 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; /** diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java index 056bb04aa..6d9b1d7a5 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java index 1071eeee2..097ac910c 100644 --- a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java +++ b/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java @@ -17,7 +17,6 @@ package ch.eitchnet.utils.exceptions; /** * @author Robert von Burg - * */ public class XmlException extends RuntimeException { private static final long serialVersionUID = 1L; diff --git a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java index fdb837ec4..5404d9e7f 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java @@ -19,7 +19,6 @@ import java.util.Arrays; /** * @author Robert von Burg - * */ public class ArraysHelper { diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java index c9657a51d..97697ee28 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java @@ -17,7 +17,6 @@ package ch.eitchnet.utils.helper; /** * @author Robert von Burg - * */ public class ByteHelper { diff --git a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java index 2b4b1f34b..6c868b889 100644 --- a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/MathHelper.java @@ -21,7 +21,8 @@ import java.math.RoundingMode; /** * A helper class that contains mathematical computations that can be used throughout. * - * @author msmock, gattom + * @author Martin Smock + * @author Michael Gatto */ public class MathHelper { diff --git a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java index e7bcd4eeb..60a236da7 100644 --- a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java +++ b/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java @@ -1,3 +1,18 @@ +/* + * 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.utils.io; /** @@ -39,7 +54,7 @@ public interface FileProgressListener { * the percent completed. Ideally the value would be 100, but in cases of errors it can be less * @param position * the position where the job finished. Ideally the value would be the same as the size given at - * {@link #begin(long)} but in case of errors it can be different + * {@link #begin(int, long, long)} but in case of errors it can be different */ public void end(int percent, long position); } diff --git a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java index b6105a200..10c0a2ca2 100644 --- a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java +++ b/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java @@ -1,3 +1,18 @@ +/* + * 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.utils.io; import java.text.MessageFormat; diff --git a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java index d5d6faf5f..96b133e07 100644 --- a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java +++ b/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java @@ -1,3 +1,18 @@ +/* + * 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.utils.io; import java.text.MessageFormat; diff --git a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java index dbd1514ef..b98f525a3 100644 --- a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java +++ b/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java @@ -1,3 +1,18 @@ +/* + * 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.utils.io; import java.io.File; diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java index edbb7f646..b3ef20ada 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java @@ -18,9 +18,9 @@ package ch.eitchnet.utils.iso8601; import java.util.Date; /** - * interface for all date formats internally used by rsp applications + * Interface for date formatting * - * @author msmock + * Martin Smock */ public interface DateFormat { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java index 411c70d9c..b5189d2e7 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java @@ -16,9 +16,9 @@ package ch.eitchnet.utils.iso8601; /** - * interface for all duration formats internally used by the platform + * Interface for duration formatting * - * @author msmock + * Martin Smock */ public interface DurationFormat { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java index 931d1c720..6dbf336f2 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java @@ -21,42 +21,42 @@ import java.util.Date; * This interface defines methods for formatting values for UI representation and also defines factory methods for * formatters for parsing and formatting duration and date values * - * @author msmock + * Martin Smock */ public interface FormatFactory { /** * return the formatter for dates * - * @return RSPDurationFormat + * @return {@link DurationFormat} */ public DateFormat getDateFormat(); /** * return the formatter for durations * - * @return RSPDurationFormat + * @return {@link DurationFormat} */ public DurationFormat getDurationFormat(); /** * return the formatter for work time * - * @return RSPWorktimeFormat + * @return {@link WorktimeFormat} */ public WorktimeFormat getWorktimeFormat(); /** * the date format used in xml import and export * - * @return RSPDateFormat + * @return {@link DateFormat} */ public DateFormat getXmlDateFormat(); /** * the duration format used in xml import and export * - * @return RSPDurationFormat + * @return {@link DurationFormat} */ public DurationFormat getXmlDurationFormat(); diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java index 2c9f1de68..327e67b2c 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory; import ch.eitchnet.utils.helper.StringHelper; /** - * + * @author Martin Smock */ @SuppressWarnings("nls") public class ISO8601 implements DateFormat { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java index c319c719e..9d4ae3320 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -42,8 +42,8 @@ package ch.eitchnet.utils.iso8601; * minutes and seconds *

* - * @author msmock - * @author gattom (reimplementation using enum) + * @author Martin Smock + * @author Michael Gatto (reimplementation using enum) */ @SuppressWarnings("nls") public class ISO8601Duration implements DurationFormat { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java index 682ad6bbd..c8ea878c7 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java @@ -22,7 +22,7 @@ import ch.eitchnet.utils.helper.MathHelper; /** * Default factory for date formats used for serialization. * - * @author msmock + * @author Martin Smock */ public class ISO8601FormatFactory implements FormatFactory { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java index 81f96cfa5..31dde2a1c 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java @@ -42,8 +42,8 @@ package ch.eitchnet.utils.iso8601; * minutes and seconds *

* - * @author msmock - * @author gattom (reimplementation using enum) + * @author Martin Smock + * @author Michael Gatto (reimplementation using enum) */ @SuppressWarnings("nls") public class ISO8601Worktime implements WorktimeFormat { diff --git a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java index 48fbf65b3..c026e060f 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java @@ -18,7 +18,7 @@ package ch.eitchnet.utils.iso8601; /** * interface for the worktime format * - * @author msmock + * @author Martin Smock */ public interface WorktimeFormat { diff --git a/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java b/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java index 2d1f65ee7..ec25837e9 100644 --- a/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java @@ -1,3 +1,18 @@ +/* + * 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 static org.junit.Assert.fail; @@ -9,17 +24,32 @@ import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/* + * 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. + */ public class AbstractEndpointTest { static final Logger logger = LoggerFactory.getLogger(FileEndpointTest.class); - public static TestIoMessage createTestMessage(String key1, String key2) { - return createTestMessage(CommandKey.key(key1, key2)); + public static TestIoMessage createTestMessage(String key1, String key2, String connectionId) { + return createTestMessage(CommandKey.key(key1, key2), connectionId); } @SuppressWarnings("nls") - public static TestIoMessage createTestMessage(CommandKey key) { - TestIoMessage msg = new TestIoMessage(UUID.randomUUID().toString(), key); + public static TestIoMessage createTestMessage(CommandKey key, String connectionId) { + TestIoMessage msg = new TestIoMessage(UUID.randomUUID().toString(), key, connectionId); List lines = new ArrayList<>(); lines.add("bla"); lines.add("foo"); diff --git a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java index 54d4f960e..9a7e41bc0 100644 --- a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java @@ -1,3 +1,18 @@ +/* + * 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 static org.junit.Assert.assertEquals; @@ -12,6 +27,9 @@ import org.slf4j.Logger; import ch.eitchnet.communication.console.ConsoleEndpoint; import ch.eitchnet.communication.console.ConsoleMessageVisitor; +/** + * @author Robert von Burg + */ public class ConsoleEndpointTest extends AbstractEndpointTest { private static final String CONNECTION_ID = "Console"; //$NON-NLS-1$ @@ -34,7 +52,7 @@ public class ConsoleEndpointTest extends AbstractEndpointTest { this.connection.start(); CommandKey key = CommandKey.key(CONNECTION_ID, "logger"); //$NON-NLS-1$ - TestIoMessage msg = createTestMessage(key); + TestIoMessage msg = createTestMessage(key, CONNECTION_ID); TestConnectionObserver observer = new TestConnectionObserver(); this.connection.addConnectionObserver(key, observer); diff --git a/src/test/java/ch/eitchnet/communication/FileEndpointTest.java b/src/test/java/ch/eitchnet/communication/FileEndpointTest.java index 454433a76..9ae899d8c 100644 --- a/src/test/java/ch/eitchnet/communication/FileEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/FileEndpointTest.java @@ -1,3 +1,18 @@ +/* + * 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 static org.junit.Assert.assertEquals; @@ -21,6 +36,9 @@ import ch.eitchnet.communication.file.FileEndpoint; import ch.eitchnet.communication.file.FileEndpointMode; import ch.eitchnet.utils.helper.FileHelper; +/** + * @author Robert von Burg + */ public class FileEndpointTest extends AbstractEndpointTest { public static final String INBOUND_FILENAME = "target/test_in.txt"; //$NON-NLS-1$ @@ -63,7 +81,7 @@ public class FileEndpointTest extends AbstractEndpointTest { // send a message this.connection.start(); TestConnectionObserver outboundObserver = new TestConnectionObserver(); - TestIoMessage message = createTestMessage(outboundFilename, FileEndpointMode.WRITE.name()); + TestIoMessage message = createTestMessage(outboundFilename, FileEndpointMode.WRITE.name(), CONNECTION_ID); this.connection.addConnectionObserver(message.getKey(), outboundObserver); this.connection.send(message); @@ -105,7 +123,7 @@ public class FileEndpointTest extends AbstractEndpointTest { lines.add(line); } return new TestIoMessage(UUID.randomUUID().toString(), CommandKey.key(this.inboundFilename, - FileEndpointMode.READ.name()), lines); + FileEndpointMode.READ.name()), CONNECTION_ID, lines); } @Override diff --git a/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java b/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java new file mode 100644 index 000000000..483041519 --- /dev/null +++ b/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java @@ -0,0 +1,70 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; + +/** + * @author Robert von Burg + */ +public class SimpleMessageArchiveTest extends AbstractEndpointTest { + + @Test + public void testArchive() throws InterruptedException { + + IoMessageArchive archive = new SimpleMessageArchive(20, 5); + + CommandKey key = CommandKey.key("key1", "key2"); //$NON-NLS-1$//$NON-NLS-2$ + String connectionId = "connection1"; //$NON-NLS-1$ + + int i = 0; + for (; i < 20; i++) { + TestIoMessage msg = new TestIoMessage(UUID.randomUUID().toString(), key, connectionId); + // update the time by plus 1, otherwise the tree set does not add it + msg.setUpdated(new Date(i + 1)); + archive.archive(msg); + } + + assertEquals(20, archive.size()); + + // add one more + TestIoMessage msg = new TestIoMessage(UUID.randomUUID().toString(), key, connectionId); + msg.setUpdated(new Date(i + 1)); + archive.archive(msg); + + // validate the trimming works + assertEquals(15, archive.size()); + + // Now make sure our last element is still in the list + List all = archive.getAll(); + Collections.sort(all, new Comparator() { + @Override + public int compare(IoMessage o1, IoMessage o2) { + return o1.getUpdated().compareTo(o2.getUpdated()); + } + }); + IoMessage message = all.get(all.size() - 1); + assertEquals(msg.getId(), message.getId()); + } +} diff --git a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java index 80227ac0a..3e9425912 100644 --- a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java @@ -1,3 +1,18 @@ +/* + * 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 static org.junit.Assert.assertEquals; @@ -22,6 +37,9 @@ import ch.eitchnet.communication.tcpip.ServerSocketEndpoint; import ch.eitchnet.communication.tcpip.SocketEndpointConstants; import ch.eitchnet.communication.tcpip.SocketMessageVisitor; +/** + * @author Robert von Burg + */ public class SocketEndpointTest extends AbstractEndpointTest { private static final String PORT = "45678"; //$NON-NLS-1$ @@ -85,7 +103,7 @@ public class SocketEndpointTest extends AbstractEndpointTest { CommandKey outboundKey = CommandKey.key(CLIENT_CONNECTION_ID, "lines"); //$NON-NLS-1$ this.clientConnection.addConnectionObserver(outboundKey, clientObserver); - TestIoMessage outboundMsg = createTestMessage(outboundKey); + TestIoMessage outboundMsg = createTestMessage(outboundKey, CLIENT_CONNECTION_ID); this.clientConnection.send(outboundMsg); waitForMessage(clientObserver); assertEquals(outboundMsg.getKey(), clientObserver.getMessage().getKey()); @@ -128,7 +146,8 @@ public class SocketEndpointTest extends AbstractEndpointTest { } logger.info(MessageFormat.format("Read {0} lines from stream.", lines.size())); //$NON-NLS-1$ - return new TestIoMessage(UUID.randomUUID().toString(), CommandKey.key(SERVER_CONNECTION_ID, "lines"), lines); //$NON-NLS-1$ + return new TestIoMessage(UUID.randomUUID().toString(), + CommandKey.key(SERVER_CONNECTION_ID, "lines"), SERVER_CONNECTION_ID, lines); //$NON-NLS-1$ } } } diff --git a/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java b/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java index a2882b7cd..36db42fc5 100644 --- a/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java +++ b/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -5,6 +20,9 @@ import java.text.MessageFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * @author Robert von Burg + */ public class TestConnectionObserver implements ConnectionObserver { private static final Logger logger = LoggerFactory.getLogger(FileEndpointTest.class); diff --git a/src/test/java/ch/eitchnet/communication/TestIoMessage.java b/src/test/java/ch/eitchnet/communication/TestIoMessage.java index cdd3d139b..c4d473d08 100644 --- a/src/test/java/ch/eitchnet/communication/TestIoMessage.java +++ b/src/test/java/ch/eitchnet/communication/TestIoMessage.java @@ -1,17 +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; import java.util.List; +/** + * @author Robert von Burg + */ public class TestIoMessage extends IoMessage { private List contents; - public TestIoMessage(String id, CommandKey key) { - super(id, key); + public TestIoMessage(String id, CommandKey key, String connectionId) { + super(id, key, connectionId); } - public TestIoMessage(String id, CommandKey key, List contents) { - super(id, key); + public TestIoMessage(String id, CommandKey key, String connectionId, List contents) { + super(id, key, connectionId); this.contents = contents; } diff --git a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java index 2f73cdb45..5877e46d2 100644 --- a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java +++ b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java @@ -1,3 +1,18 @@ +/* + * 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.utils.dbc; import java.io.File; @@ -12,7 +27,7 @@ import ch.eitchnet.utils.dbc.DBC.DbcException; * The class DBCTest contains tests for the class {@link DBC}. * * @generatedBy CodePro at 2/2/14 8:13 PM - * @author eitch + * @author Robert von Burg * @version $Revision: 1.0 $ */ @SuppressWarnings("nls") From b87784112ef8ba1b501f932e21b5829fdc8b778e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 14:42:37 +0200 Subject: [PATCH 096/180] [Project] Fixed broken imports --- .../eitchnet/communication/ConnectionMessages.java | 5 +++-- .../communication/tcpip/ClientSocketEndpoint.java | 9 +++++---- .../communication/tcpip/ServerSocketEndpoint.java | 13 +++++++------ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java index d3f4940b4..69eff51a5 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java @@ -19,7 +19,8 @@ import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ch.eitchnet.utils.helper.StringHelper; @@ -30,7 +31,7 @@ import ch.eitchnet.utils.helper.StringHelper; */ public class ConnectionMessages { - private static Logger logger = Logger.getLogger(ConnectionMessages.class); + private static Logger logger = LoggerFactory.getLogger(ConnectionMessages.class); /** * Utility class diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index 99052e43d..216964853 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -24,7 +24,8 @@ import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.Map; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ch.eitchnet.communication.CommunicationConnection; import ch.eitchnet.communication.CommunicationEndpoint; @@ -51,7 +52,7 @@ import ch.eitchnet.utils.helper.StringHelper; */ public class ClientSocketEndpoint implements CommunicationEndpoint { - protected static final Logger logger = Logger.getLogger(ClientSocketEndpoint.class); + protected static final Logger logger = LoggerFactory.getLogger(ClientSocketEndpoint.class); // state variables private boolean connected; @@ -210,7 +211,7 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { } catch (Exception e) { String msg = "Error while connecting to {0}:{1}"; //$NON-NLS-1$ logger.error(MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort))); - logger.error(e, e); + logger.error(e.getMessage(), e); this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } } @@ -511,7 +512,7 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { logger.warn("Socket has been closed!"); //$NON-NLS-1$ message.setState(State.FATAL, "Socket has been closed!"); //$NON-NLS-1$ } else { - logger.error(e, e); + logger.error(e.getMessage(), e); message.setState(State.FATAL, e.getLocalizedMessage()); this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 4619c0774..0b8d864e0 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -27,7 +27,8 @@ import java.net.UnknownHostException; import java.text.MessageFormat; import java.util.Map; -import org.apache.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ch.eitchnet.communication.CommunicationConnection; import ch.eitchnet.communication.CommunicationEndpoint; @@ -55,7 +56,7 @@ import ch.eitchnet.utils.helper.StringHelper; */ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { - protected static final Logger logger = Logger.getLogger(ServerSocketEndpoint.class); + protected static final Logger logger = LoggerFactory.getLogger(ServerSocketEndpoint.class); private Thread serverThread; @@ -228,7 +229,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { } else { String msg = "Error while opening socket for inbound connection {0}"; //$NON-NLS-1$ logger.error(MessageFormat.format(msg, this.connection.getId())); - logger.error(e, e); + logger.error(e.getMessage(), e); this.connected = false; this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } @@ -532,8 +533,8 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { this.serverSocket = new ServerSocket(this.localInputPort, 1, this.localInputAddress); this.serverSocket.setReuseAddress(true); } catch (BindException e) { - logger.fatal("Fatal BindException occurred! Port is already in use, or address is illegal!"); //$NON-NLS-1$ - logger.fatal(e, 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; @@ -559,7 +560,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { if (e instanceof InterruptedException) { logger.error("Interrupted!"); //$NON-NLS-1$ } else { - logger.error(e, e); + logger.error(e.getMessage(), e); } this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } finally { From b94423f1ce32045cda88f624708b35d78208291b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 14:44:27 +0200 Subject: [PATCH 097/180] [Minor] fixed compiler warnings about calling close() on stream --- src/main/java/ch/eitchnet/utils/helper/FileHelper.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 7d73800ec..397ed6382 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -147,9 +147,7 @@ public class FileHelper { 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) { @@ -168,10 +166,7 @@ public class FileHelper { public static final void writeStringToFile(String string, File dstFile) { try (BufferedWriter bufferedwriter = new BufferedWriter(new FileWriter(dstFile));) { - bufferedwriter.write(string); - bufferedwriter.close(); - } catch (FileNotFoundException e) { throw new RuntimeException("Filed does not exist " + dstFile.getAbsolutePath()); //$NON-NLS-1$ } catch (IOException e) { @@ -297,9 +292,7 @@ public class FileHelper { outBuffer.write(theByte); } - inBuffer.close(); outBuffer.flush(); - outBuffer.close(); if (checksum) { String fromFileMD5 = StringHelper.getHexString(FileHelper.hashFileMd5(fromFile)); @@ -554,7 +547,6 @@ public class FileHelper { complete.update(buffer, 0, numRead); } } while (numRead != -1); - fis.close(); return complete.digest(); } catch (Exception e) { From b54d16487b27a899b2622e134f1cf6571d702c66 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 15:09:24 +0200 Subject: [PATCH 098/180] [New] Added StringHelper.getUniqueId() --- .../eitchnet/utils/helper/StringHelper.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index b7f1b6302..2d90238a6 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -46,6 +46,11 @@ public class StringHelper { 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 */ @@ -629,4 +634,19 @@ public class StringHelper { throw new RuntimeException(msg); } } + + /** + * 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() { + + if (uniqueId == Long.MAX_VALUE - 1) { + uniqueId = 0; + } + + uniqueId += 1; + return Long.toString(uniqueId); + } } From 4c467df11ae1ac25038c0d920cf9341975a9e6c5 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 15:53:04 +0200 Subject: [PATCH 099/180] [New] Added AsciiHelper --- .../ch/eitchnet/utils/helper/AsciiHelper.java | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java b/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java new file mode 100644 index 000000000..b06c24474 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java @@ -0,0 +1,312 @@ +/* + * 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; + +/** + * ASCII constants + * + * @author Robert von Burg + */ +public class AsciiHelper { + + /** + * ASCII Value 0, interpretation: NUL
+ * Description: Null character + */ + public static final char NUL = (char) 0; // Null character + + /** + * ASCII Value 1, interpretation: SOH
+ * Description: Start of Header + */ + public static final char SOH = (char) 1; // Start of Header + + /** + * ASCII Value 2, interpretation: STX
+ * Description: Start of Text + */ + public static final char STX = (char) 2; // Start of Text + + /** + * ASCII Value 3, interpretation: ETX
+ * Description: End of Text + */ + public static final char ETX = (char) 3; // End of Text + + /** + * ASCII Value 4, interpretation: EOT
+ * Description: End of Transmission + */ + public static final char EOT = (char) 4; // End of Transmission + + /** + * ASCII Value 5, interpretation: ENQ
+ * Description: Enquiry + */ + public static final char ENQ = (char) 5; // Enquiry + + /** + * ASCII Value 6, interpretation: ACK
+ * Description: Acknowledgement + */ + public static final char ACK = (char) 6; // Acknowledgement + + /** + * ASCII Value 7, interpretation: BEL
+ * Description: Bell + */ + public static final char BEL = (char) 7; // Bell + + /** + * ASCII Value 8, interpretation: BS
+ * Description: Backspace + */ + public static final char BS = (char) 8; // Backspace + + /** + * ASCII Value 9, interpretation: HT
+ * Description: Horizontal Tab + */ + public static final char HT = (char) 9; // Horizontal Tab + + /** + * ASCII Value 10, interpretation: LF
+ * Description: Line Feed + */ + public static final char LF = (char) 10; // Line Feed + + /** + * ASCII Value 11, interpretation: VT
+ * Description: Vertical Tab + */ + public static final char VT = (char) 11; // Vertical Tab + + /** + * ASCII Value 12, interpretation: FF
+ * Description: Form Feed + */ + public static final char FF = (char) 12; // Form Feed + + /** + * ASCII Value 13, interpretation: CR
+ * Description: Carriage Return + */ + public static final char CR = (char) 13; // Carriage Return + + /** + * ASCII Value 14, interpretation: SO
+ * Description: Shift Out + */ + public static final char SO = (char) 14; // Shift Out + + /** + * ASCII Value 15, interpretation: SI
+ * Description: Shift In + */ + public static final char SI = (char) 15; // Shift In + + /** + * ASCII Value 16, interpretation: DLE
+ * Description: Data Link Escape + */ + public static final char DLE = (char) 16; // Data Link Escape + + /** + * ASCII Value 17, interpretation: DC1
+ * Description: (XON) Device Control 1 + */ + public static final char DC1 = (char) 17; // (XON) Device Control 1 + + /** + * ASCII Value 18, interpretation: DC2
+ * Description: Device Control 2 + */ + public static final char DC2 = (char) 18; // Device Control 2 + + /** + * ASCII Value 19 interpretation: DC3
+ * Description: (XOFF) Device Control 3 + */ + public static final char DC3 = (char) 19; // (XOFF) Device Control 3 + + /** + * ASCII Value 20, interpretation: DC4
+ * Description: Device Control 4 + */ + public static final char DC4 = (char) 20; // Device Control 4 + + /** + * ASCII Value 21, interpretation: NAK
+ * Description: Negative Acknowledgment + */ + public static final char NAK = (char) 21; // Negative Acknowledgment + + /** + * ASCII Value 22, interpretation: SYN
+ * Description: Synchronous Idle + */ + public static final char SYN = (char) 22; // Synchronous Idle + + /** + * ASCII Value 23, interpretation: ETB
+ * Description: End of Transmission Block + */ + public static final char ETB = (char) 23; // End of Transmission Block + + /** + * ASCII Value 24, interpretation: CAN
+ * Description: Cancel + */ + public static final char CAN = (char) 24; // Cancel + + /** + * ASCII Value 25, interpretation: EM
+ * Description: End of Medium + */ + public static final char EM = (char) 25; // End of Medium + + /** + * ASCII Value 26, interpretation: SUB
+ * Description: Substitute + */ + public static final char SUB = (char) 26; // Substitute + + /** + * ASCII Value 27, interpretation: ESC
+ * Description: Escape + */ + public static final char ESC = (char) 27; // Escape + + /** + * ASCII Value 28, interpretation: FS
+ * Description: File Separator + */ + public static final char FS = (char) 28; // File Separator + + /** + * ASCII Value 29, interpretation: GS
+ * Description: Group Separator + */ + public static final char GS = (char) 29; // Group Separator + + /** + * ASCII Value 30, interpretation: RS
+ * Description: Request to Send (Record Separator) + */ + public static final char RS = (char) 30; // Request to Send (Record Separator) + + /** + * ASCII Value 31, interpretation: US
+ * Description: Unit Separator + */ + public static final char US = (char) 31; // Unit Separator + + /** + * ASCII Value 32, interpretation: SP
+ * Description: Space + */ + public static final char SP = (char) 32; // Space + + /** + * ASCII Value 127, interpretation: DEL
+ * Description: Delete + */ + public static final char DEL = (char) 127; // Delete + + /** + * Returns the ASCII Text of a certain char value + * + * @param c + * @return String + */ + @SuppressWarnings("nls") + public static String getAsciiText(char c) { + // else if(c == ) { return "";} + if (c == NUL) { + return "NUL"; + } else if (c == SOH) { + return "SOH"; + } else if (c == STX) { + return "STX"; + } else if (c == ETX) { + return "ETX"; + } else if (c == EOT) { + return "EOT"; + } else if (c == ENQ) { + return "ENQ"; + } else if (c == ACK) { + return "ACK"; + } else if (c == BEL) { + return "BEL"; + } else if (c == BS) { + return "BS"; + } else if (c == HT) { + return "HT"; + } else if (c == LF) { + return "LF"; + } else if (c == VT) { + return "VT"; + } else if (c == FF) { + return "FF"; + } else if (c == CR) { + return "CR"; + } else if (c == SO) { + return "SO"; + } else if (c == SI) { + return "SI"; + } else if (c == DLE) { + return "DLE"; + } else if (c == DC1) { + return "DC1"; + } else if (c == DC2) { + return "DC2"; + } else if (c == DC3) { + return "DC3"; + } else if (c == DC4) { + return "DC4"; + } else if (c == NAK) { + return "NAK"; + } else if (c == SYN) { + return "SYN"; + } else if (c == ETB) { + return "ETB"; + } else if (c == CAN) { + return "CAN"; + } else if (c == EM) { + return "EM"; + } else if (c == SUB) { + return "SUB"; + } else if (c == ESC) { + return "ESC"; + } else if (c == FS) { + return "FS"; + } else if (c == GS) { + return "GS"; + } else if (c == RS) { + return "RS"; + } else if (c == US) { + return "US"; + } else if (c == SP) { + return "SP"; + } else if (c == DEL) { + return "DEL"; + } else if ((c) > 32 && (c) < 127) { + return String.valueOf(c); + } else { + return "(null:" + (byte) c + ")"; + } + } +} From 52a04f0c33b5b5c543ce3ac5f31d813f7c7a7760 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 15:53:32 +0200 Subject: [PATCH 100/180] [Minor] made some fields protected in tcpip endpoints --- .../communication/tcpip/ClientSocketEndpoint.java | 9 +++++---- .../communication/tcpip/ServerSocketEndpoint.java | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index 216964853..d881c541f 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -76,12 +76,13 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { // connection private Socket socket; - private DataOutputStream outputStream; - private DataInputStream inputStream; - private CommunicationConnection connection; + protected DataOutputStream outputStream; + protected DataInputStream inputStream; - private SocketMessageVisitor messageVisitor; + protected CommunicationConnection connection; + + protected SocketMessageVisitor messageVisitor; /** * Default constructor diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 0b8d864e0..ae12d8c7e 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -82,12 +82,13 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { // connection private ServerSocket serverSocket; private Socket socket; - private DataOutputStream outputStream; - private DataInputStream inputStream; - private CommunicationConnection connection; + protected DataOutputStream outputStream; + protected DataInputStream inputStream; - private SocketMessageVisitor messageVisitor; + protected CommunicationConnection connection; + + protected SocketMessageVisitor messageVisitor; /** * Default constructor From c7484934a5380f50eb7af4b8493d27ae98436fbe Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 12 Aug 2014 16:00:25 +0200 Subject: [PATCH 101/180] [New] Added AsciiHelper --- .../java/ch/eitchnet/utils/helper/AsciiHelper.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java b/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java index b06c24474..7f648de0f 100644 --- a/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java @@ -226,6 +226,16 @@ public class AsciiHelper { */ public static final char DEL = (char) 127; // Delete + /** + * Returns the ASCII Text of a certain bye value + * + * @param b + * @return String + */ + public static String getAsciiText(byte b) { + return getAsciiText((char) b); + } + /** * Returns the ASCII Text of a certain char value * From b5ac7d008f882c7bf6cbd2fa585f2b559db3348f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 14 Aug 2014 16:23:10 +0200 Subject: [PATCH 102/180] [Project] using parent version 1.1.0-SNAPSHOT --- pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 609b01d1a..793f66be1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ ch.eitchnet ch.eitchnet.parent - 0.1.0-SNAPSHOT + 1.1.0-SNAPSHOT ../ch.eitchnet.parent/pom.xml ch.eitchnet.utils - jar 0.2.0-SNAPSHOT + jar ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse https://github.com/eitchnet/ch.eitchnet.utils @@ -26,6 +26,7 @@ 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 From 863ffecd957a98c96c0c2e62a6b5785f29921fe0 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 15 Aug 2014 14:53:38 +0200 Subject: [PATCH 103/180] [New] added small chat server/client --- pom.xml | 15 ++- .../CommunicationConnection.java | 31 +++++- .../communication/ConnectionMessages.java | 14 ++- .../ConnectionStateObserver.java | 6 + .../ch/eitchnet/communication/chat/Chat.java | 104 ++++++++++++++++++ .../communication/chat/ChatClient.java | 91 +++++++++++++++ .../communication/chat/ChatIoMessage.java | 31 ++++++ .../chat/ChatMessageVisitor.java | 38 +++++++ .../communication/chat/ChatServer.java | 88 +++++++++++++++ .../tcpip/ClientSocketEndpoint.java | 14 ++- .../tcpip/ServerSocketEndpoint.java | 18 +-- .../tcpip/SocketMessageVisitor.java | 10 ++ src/main/java/log4j.xml | 30 +++++ .../communication/SocketEndpointTest.java | 9 +- 14 files changed, 467 insertions(+), 32 deletions(-) create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java create mode 100644 src/main/java/ch/eitchnet/communication/chat/Chat.java create mode 100644 src/main/java/ch/eitchnet/communication/chat/ChatClient.java create mode 100644 src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java create mode 100644 src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java create mode 100644 src/main/java/ch/eitchnet/communication/chat/ChatServer.java create mode 100644 src/main/java/log4j.xml diff --git a/pom.xml b/pom.xml index 793f66be1..f81efd194 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,11 @@ + + org.slf4j + slf4j-log4j12 + runtime + @@ -46,9 +51,9 @@ org.apache.maven.plugins maven-source-plugin - + org.apache.maven.plugins - maven-javadoc-plugin + maven-javadoc-plugin org.apache.maven.plugins @@ -57,11 +62,11 @@ org.apache.maven.plugins maven-site-plugin - - + + org.apache.maven.plugins - maven-deploy-plugin + maven-deploy-plugin diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 803dffc58..846be6086 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -48,6 +48,7 @@ public class CommunicationConnection implements Runnable { private Thread queueThread; private volatile boolean run; private MapOfLists connectionObservers; + private List connectionStateObservers; private CommunicationEndpoint endpoint; private IoMessageVisitor messageVisitor; @@ -73,6 +74,7 @@ public class CommunicationConnection implements Runnable { this.stateMsg = this.state.toString(); this.messageQueue = new LinkedBlockingDeque<>(); this.connectionObservers = new MapOfLists<>(); + this.connectionStateObservers = new ArrayList<>(); } public void setArchive(IoMessageArchive archive) { @@ -123,9 +125,31 @@ public class CommunicationConnection implements Runnable { } } + 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) { @@ -160,7 +184,6 @@ public class CommunicationConnection implements Runnable { logger.info("Started SIMULATION connection!"); //$NON-NLS-1$ break; case ON: - logger.info("Connecting..."); //$NON-NLS-1$ if (this.queueThread != null) { logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$ } else { @@ -327,8 +350,10 @@ public class CommunicationConnection implements Runnable { } synchronized (this.connectionObservers) { List observers = this.connectionObservers.getList(message.getKey()); - for (ConnectionObserver observer : observers) { - observer.notify(message.getKey(), message); + if (observers != null) { + for (ConnectionObserver observer : observers) { + observer.notify(message.getKey(), message); + } } } } diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java index 69eff51a5..18aae76ad 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java @@ -72,7 +72,7 @@ public class ConnectionMessages { value = StringHelper.NULL; String msg = "{0}: parameter ''{1}'' has invalid value ''{2}''"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, clazz.getName(), parameterName, value); + msg = MessageFormat.format(msg, clazz.getSimpleName(), parameterName, value); ConnectionException e = new ConnectionException(msg); return e; } @@ -88,7 +88,7 @@ public class ConnectionMessages { */ 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.getName(), parameter1, parameter1); + msg = MessageFormat.format(msg, clazz.getSimpleName(), parameter1, parameter1); ConnectionException e = new ConnectionException(msg); return e; } @@ -101,10 +101,12 @@ public class ConnectionMessages { * @param defValue */ public static void warnUnsetParameter(Class clazz, String parameterName, String defValue) { - String msg = "{0}: parameter ''{1}'' is not set, using default value ''{2}''"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, clazz.getName(), parameterName, defValue); - Map properties = new HashMap(); - logger.warn(MessageFormat.format(msg, properties)); + 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)); + } } /** diff --git a/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java b/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java new file mode 100644 index 000000000..3b98f1f17 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/communication/chat/Chat.java b/src/main/java/ch/eitchnet/communication/chat/Chat.java new file mode 100644 index 000000000..2e53e2a73 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/chat/Chat.java @@ -0,0 +1,104 @@ +package ch.eitchnet.communication.chat; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.MessageFormat; + +import ch.eitchnet.utils.helper.StringHelper; + +public class Chat { + + public static void main(String[] args) { + + if (args.length < 3) + printIllegalArgsAndExit(args); + + if (args[0].equals("server")) { //$NON-NLS-1$ + if (args.length != 3) + printIllegalArgsAndExit(args); + startServer(args); + } else if (args[0].equals("client")) { //$NON-NLS-1$ + if (args.length != 4) + printIllegalArgsAndExit(args); + startClient(args); + } + } + + private static void startServer(String[] args) { + + // port + int port; + String portS = args[1]; + 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[2]; + + // start + ChatServer chatServer = new ChatServer(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/src/main/java/ch/eitchnet/communication/chat/ChatClient.java b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java new file mode 100644 index 000000000..903c45fef --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java @@ -0,0 +1,91 @@ +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_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) { + this.notifyAll(); + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java b/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java new file mode 100644 index 000000000..a9bf851eb --- /dev/null +++ b/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/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java new file mode 100644 index 000000000..6589bb389 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java @@ -0,0 +1,38 @@ +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 { + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + String line = bufferedReader.readLine(); + 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.writeBytes(chatIoMessage.getChatMsg()); + outputStream.writeByte('\n'); + outputStream.flush(); + } +} diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java new file mode 100644 index 000000000..3fd82ea84 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java @@ -0,0 +1,88 @@ +package ch.eitchnet.communication.chat; + +import static ch.eitchnet.communication.chat.ChatMessageVisitor.inboundKey; +import static ch.eitchnet.communication.chat.ChatMessageVisitor.outboundKey; + +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 int port; + private String username; + private boolean connected; + + public ChatServer(int port, String username) { + this.port = port; + this.username = username; + } + + public void start() { + ConnectionMode mode = ConnectionMode.ON; + + Map parameters = new HashMap<>(); + parameters.put(SocketEndpointConstants.PARAMETER_USE_TIMEOUT, Boolean.FALSE.toString()); + 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) { + this.notifyAll(); + } + } +} diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index d881c541f..c58926f3b 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -173,9 +173,11 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { } // configure the socket - 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())); + 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); @@ -196,7 +198,7 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { this.inputStream.skip(available); } - msg = "Connected {0}: {1}:{2} with local side {3}:{4}"; //$NON-NLS-1$ + 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()))); @@ -325,7 +327,7 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { // if local output address is not set, then we will use the localhost InetAddress if (this.localOutputAddressS == null || this.localOutputAddressS.length() == 0) { - logger.warn("No localOutputAddress set. Using localhost"); //$NON-NLS-1$ + logger.debug("No localOutputAddress set. Using localhost"); //$NON-NLS-1$ } else { // parse local output address name to InetAddress object @@ -462,7 +464,7 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { 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$ + // 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) { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index ae12d8c7e..8cc5b71ec 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -187,9 +187,11 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { } // configure the socket - 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())); + 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); @@ -344,7 +346,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { // if remote address is not set, then we will use the localhost InetAddress if (this.remoteOutputAddressS == null || this.remoteOutputAddressS.length() == 0) { - logger.warn("No remoteOutputAddress set. Allowing connection from any remote address"); //$NON-NLS-1$ + logger.debug("No remoteOutputAddress set. Allowing connection from any remote address"); //$NON-NLS-1$ } else { // parse remote output address name to InetAddress object @@ -464,7 +466,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { 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$ + // logger.info(MessageFormat.format("Enabling connection {0}...", this.connection.getId())); //$NON-NLS-1$ this.closed = false; this.serverThread = new Thread(this, this.connection.getId()); @@ -528,9 +530,9 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { 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))); + // 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) { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java index 6d9b1d7a5..1c9610820 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java @@ -23,6 +23,16 @@ import ch.eitchnet.communication.IoMessageVisitor; public abstract class SocketMessageVisitor extends IoMessageVisitor { + protected final String connectionId; + + public SocketMessageVisitor(String connectionId) { + this.connectionId = connectionId; + } + + public String getConnectionId() { + return this.connectionId; + } + public abstract IoMessage visit(DataInputStream inputStream, DataOutputStream outputStream) throws Exception; public abstract void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message) diff --git a/src/main/java/log4j.xml b/src/main/java/log4j.xml new file mode 100644 index 000000000..0a2a73d06 --- /dev/null +++ b/src/main/java/log4j.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java index 3e9425912..d2db3c0b5 100644 --- a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java @@ -61,7 +61,7 @@ public class SocketEndpointTest extends AbstractEndpointTest { parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.TRUE.toString()); CommunicationEndpoint endpoint = new ClientSocketEndpoint(); - SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(); + SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(CLIENT_CONNECTION_ID); this.clientConnection = new CommunicationConnection(CLIENT_CONNECTION_ID, ConnectionMode.ON, parameters, endpoint, messageVisitor); this.clientConnection.configure(); @@ -73,7 +73,7 @@ public class SocketEndpointTest extends AbstractEndpointTest { parameters.put(SocketEndpointConstants.PARAMETER_LOCAL_INPUT_PORT, PORT); CommunicationEndpoint endpoint = new ServerSocketEndpoint(); - SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(); + SocketMessageVisitor messageVisitor = new SocketMessageVisitorExtension(SERVER_CONNECTION_ID); this.serverConnection = new CommunicationConnection(SERVER_CONNECTION_ID, ConnectionMode.ON, parameters, endpoint, messageVisitor); this.serverConnection.configure(); @@ -114,8 +114,9 @@ public class SocketEndpointTest extends AbstractEndpointTest { } private final class SocketMessageVisitorExtension extends SocketMessageVisitor { - public SocketMessageVisitorExtension() { - // do nothing + + public SocketMessageVisitorExtension(String connectionId) { + super(connectionId); } @Override From 3283e0e0cfd8250670efa5264d0d437e0f9054c7 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 15 Aug 2014 15:11:32 +0200 Subject: [PATCH 104/180] [New] added small chat server/client --- .../java/ch/eitchnet/communication/chat/ChatClient.java | 1 + .../ch/eitchnet/communication/chat/ChatMessageVisitor.java | 7 +++++++ .../java/ch/eitchnet/communication/chat/ChatServer.java | 1 + 3 files changed, 9 insertions(+) diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatClient.java b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java index 903c45fef..51b2c4823 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatClient.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java @@ -37,6 +37,7 @@ public class ChatClient implements ConnectionObserver, ConnectionStateObserver { 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)); diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java index 6589bb389..8737289bb 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java @@ -21,8 +21,15 @@ public class ChatMessageVisitor extends SocketMessageVisitor { @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; diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java index 3fd82ea84..9a0259890 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java @@ -34,6 +34,7 @@ public class ChatServer implements ConnectionObserver, ConnectionStateObserver { 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_PORT, Integer.toString(this.port)); parameters.put(SocketEndpointConstants.PARAMETER_CLOSE_AFTER_SEND, Boolean.FALSE.toString()); From a1a2b814086d1bac6dfbe89ae3df84ff0c4f1369 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 15 Aug 2014 15:34:57 +0200 Subject: [PATCH 105/180] [New] added small chat server/client --- .../ch/eitchnet/communication/chat/Chat.java | 47 +++++++++++++++---- .../chat/ChatMessageVisitor.java | 4 +- .../communication/chat/ChatServer.java | 6 ++- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/chat/Chat.java b/src/main/java/ch/eitchnet/communication/chat/Chat.java index 2e53e2a73..5dd90d451 100644 --- a/src/main/java/ch/eitchnet/communication/chat/Chat.java +++ b/src/main/java/ch/eitchnet/communication/chat/Chat.java @@ -1,8 +1,11 @@ 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; @@ -10,25 +13,51 @@ public class Chat { public static void main(String[] args) { - if (args.length < 3) + if (args.length < 4) printIllegalArgsAndExit(args); if (args[0].equals("server")) { //$NON-NLS-1$ - if (args.length != 3) - printIllegalArgsAndExit(args); startServer(args); } else if (args[0].equals("client")) { //$NON-NLS-1$ - if (args.length != 4) - printIllegalArgsAndExit(args); 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[1]; + String portS = args[2]; try { port = Integer.parseInt(portS); } catch (NumberFormatException e) { @@ -43,10 +72,10 @@ public class Chat { } // username - String username = args[2]; + String username = args[3]; // start - ChatServer chatServer = new ChatServer(port, username); + ChatServer chatServer = new ChatServer(host, port, username); chatServer.start(); } @@ -97,7 +126,7 @@ public class Chat { } } System.err.println(); - System.err.println("Usage: java ...Chat server "); //$NON-NLS-1$ + 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/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java index 8737289bb..29667480f 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java @@ -38,8 +38,8 @@ public class ChatMessageVisitor extends SocketMessageVisitor { @Override public void visit(DataInputStream inputStream, DataOutputStream outputStream, IoMessage message) throws Exception { ChatIoMessage chatIoMessage = (ChatIoMessage) message; - outputStream.writeBytes(chatIoMessage.getChatMsg()); - outputStream.writeByte('\n'); + outputStream.writeChars(chatIoMessage.getChatMsg()); + outputStream.writeChar('\n'); outputStream.flush(); } } diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java index 9a0259890..d94409626 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java @@ -3,6 +3,7 @@ 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; @@ -21,11 +22,13 @@ 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(int port, String username) { + public ChatServer(InetAddress address, int port, String username) { + this.address = address; this.port = port; this.username = username; } @@ -36,6 +39,7 @@ public class ChatServer implements ConnectionObserver, ConnectionStateObserver { 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()); From 34630585a15592f3922bbcc13ff913cfda4fff1c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 22 Aug 2014 12:51:43 +0200 Subject: [PATCH 106/180] [New] Added a DateRange class --- .../eitchnet/utils/collections/DateRange.java | 118 +++++++++++++++++ .../utils/collections/DateRangeTest.java | 121 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/collections/DateRange.java create mode 100644 src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java diff --git a/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/src/main/java/ch/eitchnet/utils/collections/DateRange.java new file mode 100644 index 000000000..ac1dc5cf2 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/collections/DateRange.java @@ -0,0 +1,118 @@ +/* + * 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; + +/** + * @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 (toDate != null && fromDate != null) + DBC.INTERIM.assertTrue("From must be before to!", toDate.compareTo(fromDate) >= 0); + } + + /** + * @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; + } + + public boolean contains(Date date) { + DBC.PRE.assertNotNull("Date must be given!", date); + 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 (toInclusive) + toContains = compare >= 0; + else + toContains = compare > 0; + } + + if (this.fromDate != null) { + int compare = this.fromDate.compareTo(date); + if (fromInclusive) + fromContains = compare <= 0; + else + fromContains = compare < 0; + } + return toContains && fromContains; + } +} diff --git a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java new file mode 100644 index 000000000..e8b553f6b --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.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.collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Date; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import ch.eitchnet.utils.dbc.DBC.DbcException; + +public class DateRangeTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testFrom() { + Date now = new Date(); + DateRange dateRange = new DateRange(); + dateRange.from(now, true); + assertEquals(now, dateRange.getFromDate()); + assertNull(dateRange.getToDate()); + assertFalse(dateRange.isUnbounded()); + assertFalse(dateRange.isBounded()); + } + + /** + * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date)}. + */ + @Test + public void testTo() { + Date now = new Date(); + DateRange dateRange = new DateRange(); + dateRange.to(now, true); + assertEquals(now, dateRange.getToDate()); + assertNull(dateRange.getFromDate()); + assertFalse(dateRange.isUnbounded()); + assertFalse(dateRange.isBounded()); + } + + /** + * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date)}. + */ + @Test + public void testFromTo() { + Date from = new Date(); + Date to = new Date(); + DateRange dateRange = new DateRange(); + dateRange.from(from, true).to(to, true); + assertEquals(from, dateRange.getFromDate()); + assertEquals(to, dateRange.getToDate()); + assertFalse(dateRange.isUnbounded()); + assertTrue(dateRange.isBounded()); + } + + @Test + public void shouldNotOverlap() { + exception.expect(DbcException.class); + Date from = new Date(10); + Date to = new Date(20); + DateRange dateRange = new DateRange(); + dateRange.from(to, true).to(from, true); + } + + /** + * Test method for {@link ch.eitchnet.utils.collections.DateRange#contains(java.util.Date)}. + */ + @Test + public void testContains() { + Date from = new Date(10); + Date to = new Date(20); + DateRange dateRange = new DateRange(); + dateRange.from(from, false).to(to, false); + + Date earlier = new Date(5); + Date later = new Date(25); + Date contained = new Date(15); + + assertFalse(dateRange.contains(earlier)); + assertFalse(dateRange.contains(later)); + assertTrue(dateRange.contains(contained)); + + assertFalse(dateRange.contains(from)); + assertFalse(dateRange.contains(to)); + + dateRange = new DateRange(); + dateRange.from(from, true).to(to, true); + assertTrue(dateRange.contains(from)); + assertTrue(dateRange.contains(to)); + + dateRange = new DateRange(); + dateRange.from(from, false).to(to, true); + assertFalse(dateRange.contains(from)); + assertTrue(dateRange.contains(to)); + + dateRange = new DateRange(); + dateRange.from(from, true).to(to, false); + assertTrue(dateRange.contains(from)); + assertFalse(dateRange.contains(to)); + } +} From 08a92abde7f5a36b263cb70778bad88292b6396e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 22 Aug 2014 12:52:05 +0200 Subject: [PATCH 107/180] [New] Added StringHelper.getUniqueIdLong --- .../java/ch/eitchnet/utils/helper/StringHelper.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 2d90238a6..ee2755922 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -641,12 +641,21 @@ public class StringHelper { * @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 Long.toString(uniqueId); + return uniqueId; } } From c4c5dd01d49fd2f01475748df40121525ecc7c56 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 24 Aug 2014 17:21:40 +0200 Subject: [PATCH 108/180] [Project] set version to 1.0.0-SNAPSHOT --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index f81efd194..4b32d8547 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ ch.eitchnet ch.eitchnet.parent - 1.1.0-SNAPSHOT + 1.0.0 ../ch.eitchnet.parent/pom.xml ch.eitchnet.utils - 0.2.0-SNAPSHOT + 1.0.0-SNAPSHOT jar ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse From 3f68aa0adc3b47dcbdeb24104384bf9e859a25e4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 25 Aug 2014 22:00:34 +0200 Subject: [PATCH 109/180] [New] Added StringMatchMode to use when matching strings --- .../ch/eitchnet/utils/StringMatchMode.java | 61 +++++++++++++++ .../eitchnet/utils/StringMatchModeTest.java | 75 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/StringMatchMode.java create mode 100644 src/test/java/ch/eitchnet/utils/StringMatchModeTest.java diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/src/main/java/ch/eitchnet/utils/StringMatchMode.java new file mode 100644 index 000000000..a97e2a016 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/StringMatchMode.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * @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) { + if (!this.isEquals() && !this.isCaseSensitve()) + return value1.toLowerCase().contains(value2.toLowerCase()); + + if (!this.isCaseSensitve()) + return value1.toLowerCase().equals(value2.toLowerCase()); + + if (!this.isEquals()) + return value1.contains(value2); + + return value1.equals(value2); + } +} diff --git a/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java b/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java new file mode 100644 index 000000000..aa80dc33f --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java @@ -0,0 +1,75 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * @author Robert von Burg + * + */ +public class StringMatchModeTest { + + /** + * Test method for {@link ch.eitchnet.utils.StringMatchMode#isCaseSensitve()}. + */ + @Test + public void testIsCaseSensitve() { + assertFalse(StringMatchMode.EQUALS_CASE_INSENSITIVE.isCaseSensitve()); + assertTrue(StringMatchMode.EQUALS_CASE_SENSITIVE.isCaseSensitve()); + assertFalse(StringMatchMode.CONTAINS_CASE_INSENSITIVE.isCaseSensitve()); + assertTrue(StringMatchMode.CONTAINS_CASE_SENSITIVE.isCaseSensitve()); + } + + /** + * Test method for {@link ch.eitchnet.utils.StringMatchMode#isEquals()}. + */ + @Test + public void testIsEquals() { + assertTrue(StringMatchMode.EQUALS_CASE_INSENSITIVE.isEquals()); + assertTrue(StringMatchMode.EQUALS_CASE_SENSITIVE.isEquals()); + assertFalse(StringMatchMode.CONTAINS_CASE_INSENSITIVE.isEquals()); + assertFalse(StringMatchMode.CONTAINS_CASE_SENSITIVE.isEquals()); + } + + /** + * Test method for {@link ch.eitchnet.utils.StringMatchMode#matches(java.lang.String, java.lang.String)}. + */ + @Test + public void testMatches() { + assertTrue(StringMatchMode.CONTAINS_CASE_INSENSITIVE.matches("hello", "el")); + assertTrue(StringMatchMode.CONTAINS_CASE_SENSITIVE.matches("hello", "el")); + assertFalse(StringMatchMode.CONTAINS_CASE_SENSITIVE.matches("hello", "ael")); + assertFalse(StringMatchMode.CONTAINS_CASE_SENSITIVE.matches("hello", "EL")); + assertFalse(StringMatchMode.CONTAINS_CASE_SENSITIVE.matches("hello", "aEL")); + assertTrue(StringMatchMode.CONTAINS_CASE_INSENSITIVE.matches("hello", "EL")); + + assertTrue(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "ab")); + assertFalse(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "abc")); + assertTrue(StringMatchMode.EQUALS_CASE_INSENSITIVE.matches("ab", "ab")); + assertTrue(StringMatchMode.EQUALS_CASE_INSENSITIVE.matches("ab", "AB")); + assertTrue(StringMatchMode.EQUALS_CASE_INSENSITIVE.matches("ab", "aB")); + + assertFalse(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "aB")); + assertFalse(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "aB")); + assertFalse(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "AB")); + assertFalse(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "aba")); + assertTrue(StringMatchMode.EQUALS_CASE_SENSITIVE.matches("ab", "ab")); + } +} From aa311f3cb2c58d41642c195e70e2d0442cbefa56 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 25 Aug 2014 22:51:52 +0200 Subject: [PATCH 110/180] [New] Added StringMatchMode to use when matching strings --- src/main/java/ch/eitchnet/utils/StringMatchMode.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/src/main/java/ch/eitchnet/utils/StringMatchMode.java index a97e2a016..4d4de227f 100644 --- a/src/main/java/ch/eitchnet/utils/StringMatchMode.java +++ b/src/main/java/ch/eitchnet/utils/StringMatchMode.java @@ -15,6 +15,8 @@ */ package ch.eitchnet.utils; +import ch.eitchnet.utils.dbc.DBC; + /** * @author Robert von Burg */ @@ -47,6 +49,8 @@ public enum StringMatchMode { } public boolean matches(String value1, String value2) { + DBC.PRE.assertNotNull("value1 must be set!", value1); + DBC.PRE.assertNotNull("value2 must be set!", value2); if (!this.isEquals() && !this.isCaseSensitve()) return value1.toLowerCase().contains(value2.toLowerCase()); From fe047da55e707c3af9ddb2dc5bd1be2101a13da0 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 26 Aug 2014 17:20:27 +0200 Subject: [PATCH 111/180] [New] added DateRange.isDate() --- .../eitchnet/utils/collections/DateRange.java | 7 +++++++ .../utils/collections/DateRangeTest.java | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/src/main/java/ch/eitchnet/utils/collections/DateRange.java index ac1dc5cf2..a45cb6842 100644 --- a/src/main/java/ch/eitchnet/utils/collections/DateRange.java +++ b/src/main/java/ch/eitchnet/utils/collections/DateRange.java @@ -90,6 +90,13 @@ public class DateRange { 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); if (this.fromDate == null && this.toDate == null) diff --git a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java index e8b553f6b..d70658768 100644 --- a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java +++ b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java @@ -82,6 +82,23 @@ public class DateRangeTest { dateRange.from(to, true).to(from, true); } + /** + * Test method for {@link ch.eitchnet.utils.collections.DateRange#isDate()}. + */ + @Test + public void testIsDate() { + + Date from = new Date(10); + Date to = new Date(20); + DateRange dateRange = new DateRange(); + dateRange.from(from, false).to(to, false); + assertFalse(dateRange.isDate()); + + dateRange = new DateRange(); + dateRange.from(from, false).to(from, false); + assertTrue(dateRange.isDate()); + } + /** * Test method for {@link ch.eitchnet.utils.collections.DateRange#contains(java.util.Date)}. */ From 8b6410ff2a5d2af07f736af9c6abc7c9c7ebad08 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 28 Aug 2014 21:44:35 +0200 Subject: [PATCH 112/180] [New] Added StringHelper.commaSeparated() and .splitCommaSeparated() --- .../ch/eitchnet/utils/helper/StringHelper.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index ee2755922..cc95d2ae1 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -635,6 +635,24 @@ public class StringHelper { } } + 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(", "); + } + return sb.toString(); + } + + public static String[] splitCommaSeparated(String values) { + String[] split = values.split(","); + for (int i = 0; i < split.length; i++) { + split[i] = split[i].trim(); + } + return split; + } + /** * Return a pseudo unique id which is incremented on each call. The id is initialized from the current time * From b5587d3dc3ffa28338181a9fcfe6937fa6d34747 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 29 Aug 2014 14:22:21 +0200 Subject: [PATCH 113/180] [New] added IoMessageStateObserver --- .../communication/IoMessageStateObserver.java | 48 +++++++++++++++++++ .../communication/IoMessageVisitor.java | 5 ++ .../tcpip/ClientSocketEndpoint.java | 2 + 3 files changed, 55 insertions(+) create mode 100644 src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java diff --git a/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java b/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java new file mode 100644 index 000000000..984fcbc0e --- /dev/null +++ b/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) { + this.notifyAll(); + } + } + } + + public State getState() { + return this.state; + } +} diff --git a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java index c7505ce88..5139afe87 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java @@ -15,6 +15,9 @@ */ package ch.eitchnet.communication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ch.eitchnet.communication.console.ConsoleMessageVisitor; /** @@ -32,6 +35,8 @@ import ch.eitchnet.communication.console.ConsoleMessageVisitor; */ public abstract class IoMessageVisitor { + protected static final Logger logger = LoggerFactory.getLogger(IoMessageVisitor.class); + protected CommunicationConnection connection; public void configure(CommunicationConnection connection) { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index c58926f3b..0c0a37cc2 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -283,6 +283,8 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { *
  • 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) From 09e6e06078bd3a5ec36b44d6ec6fadab24e68f36 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 13 Sep 2014 18:34:24 +0200 Subject: [PATCH 114/180] [New] Added ConnectionInfo --- .../CommunicationConnection.java | 8 +- .../communication/ConnectionInfo.java | 154 ++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ch/eitchnet/communication/ConnectionInfo.java diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 846be6086..2e0180743 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -187,7 +187,7 @@ public class CommunicationConnection implements Runnable { if (this.queueThread != null) { logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$ } else { - logger.info(MessageFormat.format("Starting Connection {0}...", this.id)); //$NON-NLS-1$ + logger.info(MessageFormat.format("Starting Connection {0} to {1}...", this.id, this.getRemoteUri())); //$NON-NLS-1$ this.run = true; this.queueThread = new Thread(this, MessageFormat.format("{0}_OUT", this.id)); //$NON-NLS-1$ this.queueThread.start(); @@ -358,10 +358,14 @@ public class CommunicationConnection implements Runnable { } } - public String getUri() { + 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(); diff --git a/src/main/java/ch/eitchnet/communication/ConnectionInfo.java b/src/main/java/ch/eitchnet/communication/ConnectionInfo.java new file mode 100644 index 000000000..d34b4e4e9 --- /dev/null +++ b/src/main/java/ch/eitchnet/communication/ConnectionInfo.java @@ -0,0 +1,154 @@ +/* + * 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 = "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 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.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; + } +} From 56126c97c2072f8cc637d5b5a239521fcf7ea7e4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 13 Sep 2014 19:01:57 +0200 Subject: [PATCH 115/180] [Minor] code cleanup --- .../ch/eitchnet/communication/ConnectionMessages.java | 2 +- src/main/java/ch/eitchnet/utils/StringMatchMode.java | 4 ++-- .../java/ch/eitchnet/utils/collections/DateRange.java | 10 +++++----- .../java/ch/eitchnet/utils/helper/StringHelper.java | 4 ++-- .../java/ch/eitchnet/utils/StringMatchModeTest.java | 1 + .../ch/eitchnet/utils/collections/DateRangeTest.java | 6 +++--- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java index 18aae76ad..28f6baef1 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMessages.java @@ -104,7 +104,7 @@ public class ConnectionMessages { 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(); + Map properties = new HashMap<>(); logger.warn(MessageFormat.format(msg, properties)); } } diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/src/main/java/ch/eitchnet/utils/StringMatchMode.java index 4d4de227f..ebea91029 100644 --- a/src/main/java/ch/eitchnet/utils/StringMatchMode.java +++ b/src/main/java/ch/eitchnet/utils/StringMatchMode.java @@ -49,8 +49,8 @@ public enum StringMatchMode { } public boolean matches(String value1, String value2) { - DBC.PRE.assertNotNull("value1 must be set!", value1); - DBC.PRE.assertNotNull("value2 must be set!", value2); + DBC.PRE.assertNotNull("value1 must be set!", value1); //$NON-NLS-1$ + DBC.PRE.assertNotNull("value2 must be set!", value2); //$NON-NLS-1$ if (!this.isEquals() && !this.isCaseSensitve()) return value1.toLowerCase().contains(value2.toLowerCase()); diff --git a/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/src/main/java/ch/eitchnet/utils/collections/DateRange.java index a45cb6842..3c25b4bc7 100644 --- a/src/main/java/ch/eitchnet/utils/collections/DateRange.java +++ b/src/main/java/ch/eitchnet/utils/collections/DateRange.java @@ -44,8 +44,8 @@ public class DateRange { } private void validate() { - if (toDate != null && fromDate != null) - DBC.INTERIM.assertTrue("From must be before to!", toDate.compareTo(fromDate) >= 0); + if (this.toDate != null && this.fromDate != null) + DBC.INTERIM.assertTrue("From must be before to!", this.toDate.compareTo(this.fromDate) >= 0); //$NON-NLS-1$ } /** @@ -98,7 +98,7 @@ public class DateRange { } public boolean contains(Date date) { - DBC.PRE.assertNotNull("Date must be given!", date); + DBC.PRE.assertNotNull("Date must be given!", date); //$NON-NLS-1$ if (this.fromDate == null && this.toDate == null) return true; @@ -107,7 +107,7 @@ public class DateRange { if (this.toDate != null) { int compare = this.toDate.compareTo(date); - if (toInclusive) + if (this.toInclusive) toContains = compare >= 0; else toContains = compare > 0; @@ -115,7 +115,7 @@ public class DateRange { if (this.fromDate != null) { int compare = this.fromDate.compareTo(date); - if (fromInclusive) + if (this.fromInclusive) fromContains = compare <= 0; else fromContains = compare < 0; diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index cc95d2ae1..526f63f4a 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -640,13 +640,13 @@ public class StringHelper { for (int i = 0; i < values.length; i++) { sb.append(values[i]); if (i < values.length - 1) - sb.append(", "); + sb.append(", "); //$NON-NLS-1$ } return sb.toString(); } public static String[] splitCommaSeparated(String values) { - String[] split = values.split(","); + String[] split = values.split(","); //$NON-NLS-1$ for (int i = 0; i < split.length; i++) { split[i] = split[i].trim(); } diff --git a/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java b/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java index aa80dc33f..c387ac305 100644 --- a/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java +++ b/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java @@ -51,6 +51,7 @@ public class StringMatchModeTest { /** * Test method for {@link ch.eitchnet.utils.StringMatchMode#matches(java.lang.String, java.lang.String)}. */ + @SuppressWarnings("nls") @Test public void testMatches() { assertTrue(StringMatchMode.CONTAINS_CASE_INSENSITIVE.matches("hello", "el")); diff --git a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java index d70658768..5ec0bbc75 100644 --- a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java +++ b/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java @@ -45,7 +45,7 @@ public class DateRangeTest { } /** - * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date)}. + * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date, boolean)}. */ @Test public void testTo() { @@ -59,7 +59,7 @@ public class DateRangeTest { } /** - * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date)}. + * Test method for {@link ch.eitchnet.utils.collections.DateRange#to(java.util.Date,boolean)}. */ @Test public void testFromTo() { @@ -75,7 +75,7 @@ public class DateRangeTest { @Test public void shouldNotOverlap() { - exception.expect(DbcException.class); + this.exception.expect(DbcException.class); Date from = new Date(10); Date to = new Date(20); DateRange dateRange = new DateRange(); From 2159c68a7197adab100fcf644db4e74660642db0 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 14 Sep 2014 11:25:23 +0200 Subject: [PATCH 116/180] [New] Added SIMULATION handling in CommunicationEndpoint and Visitor --- .../CommunicationConnection.java | 13 ++++++------- .../communication/CommunicationEndpoint.java | 4 +++- .../communication/ConnectionInfo.java | 19 +++++++++++++++++++ .../communication/IoMessageVisitor.java | 4 ++++ .../console/ConsoleEndpoint.java | 5 +++++ .../communication/file/FileEndpoint.java | 5 +++++ .../tcpip/ClientSocketEndpoint.java | 5 +++++ .../tcpip/ServerSocketEndpoint.java | 5 +++++ 8 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 2e0180743..05c75681f 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -288,7 +288,10 @@ public class CommunicationConnection implements Runnable { message = this.messageQueue.take(); logger.info(MessageFormat.format("Processing message {0}...", message.getId())); //$NON-NLS-1$ - this.endpoint.send(message); + 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) @@ -398,11 +401,7 @@ public class CommunicationConnection implements Runnable { message.setState(State.PENDING, State.PENDING.name()); - if (this.mode == ConnectionMode.SIMULATION) { - message.setState(State.DONE, State.DONE.name()); - done(message); - } else { - this.messageQueue.add(message); - } + this.messageQueue.add(message); + this.messageQueue.add(message); } } diff --git a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java index ef0a75813..54579f7b9 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java @@ -25,7 +25,7 @@ public interface CommunicationEndpoint { public void start(); public void stop(); - + public void reset(); public String getLocalUri(); @@ -33,4 +33,6 @@ public interface CommunicationEndpoint { public String getRemoteUri(); public void send(IoMessage message) throws Exception; + + public void simulate(IoMessage message) throws Exception; } diff --git a/src/main/java/ch/eitchnet/communication/ConnectionInfo.java b/src/main/java/ch/eitchnet/communication/ConnectionInfo.java index d34b4e4e9..c5bfd51a3 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionInfo.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionInfo.java @@ -33,6 +33,9 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlAccessorType(XmlAccessType.FIELD) public class ConnectionInfo { + @XmlAttribute(name = "id") + private String id; + @XmlAttribute(name = "localUri") private String localUri; @@ -51,6 +54,21 @@ public class ConnectionInfo { @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 */ @@ -143,6 +161,7 @@ public class ConnectionInfo { 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()); diff --git a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java index 5139afe87..a64207ec1 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java @@ -42,4 +42,8 @@ public abstract class IoMessageVisitor { public void configure(CommunicationConnection connection) { this.connection = connection; } + + public void simulate(IoMessage ioMessage) { + // allow for subclasses to implement + } } diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java index 11bdd4e3d..e39916c48 100644 --- a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java @@ -75,4 +75,9 @@ public class ConsoleEndpoint implements CommunicationEndpoint { this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString()); } } + + @Override + public void simulate(IoMessage message) throws Exception { + this.send(message); + } } diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java index a320a10d7..edd9b0fd1 100644 --- a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java @@ -173,6 +173,11 @@ public class FileEndpoint implements CommunicationEndpoint, Runnable { } } + @Override + public void simulate(IoMessage message) throws Exception { + this.messageVisitor.simulate(message); + } + @Override public void run() { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index 0c0a37cc2..4d7945ad2 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -497,6 +497,11 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { 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 { diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 8cc5b71ec..e6f3e02e1 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -582,6 +582,11 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { } } + @Override + public void simulate(IoMessage message) throws Exception { + this.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$ From bf72c969054056c3db6acf16d1a58ed74d45de83 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 14 Sep 2014 12:19:41 +0200 Subject: [PATCH 117/180] [Minor] code cleanup --- .../communication/CommunicationConnection.java | 4 ++-- .../communication/IoMessageStateObserver.java | 2 +- .../eitchnet/communication/chat/ChatClient.java | 2 +- .../eitchnet/communication/chat/ChatServer.java | 2 +- .../communication/console/ConsoleEndpoint.java | 4 ++-- .../tcpip/ServerSocketEndpoint.java | 2 +- .../tcpip/SocketEndpointConstants.java | 2 +- .../java/ch/eitchnet/fileserver/FileClient.java | 4 ++-- .../java/ch/eitchnet/utils/StringMatchMode.java | 6 +++--- .../communication/ConsoleEndpointTest.java | 2 +- .../utils/objectfilter/ObjectFilterTest.java | 16 ++++++++-------- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 05c75681f..a98b0edf4 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -170,7 +170,7 @@ public class CommunicationConnection implements Runnable { public void configure() { this.messageVisitor.configure(this); this.endpoint.configure(this, this.messageVisitor); - this.notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.name()); + notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.name()); } public void start() { @@ -187,7 +187,7 @@ public class CommunicationConnection implements Runnable { 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, this.getRemoteUri())); //$NON-NLS-1$ + 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(); diff --git a/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java b/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java index 984fcbc0e..cf7d11a54 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java +++ b/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java @@ -37,7 +37,7 @@ public class IoMessageStateObserver implements ConnectionObserver { this.state = message.getState(); synchronized (this) { - this.notifyAll(); + notifyAll(); } } } diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatClient.java b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java index 51b2c4823..5f0807281 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatClient.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatClient.java @@ -86,7 +86,7 @@ public class ChatClient implements ConnectionObserver, ConnectionStateObserver { this.connected = newState == ConnectionState.CONNECTED || newState == ConnectionState.IDLE || newState == ConnectionState.WORKING; synchronized (this) { - this.notifyAll(); + notifyAll(); } } } diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java index d94409626..fbd9b3bde 100644 --- a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java +++ b/src/main/java/ch/eitchnet/communication/chat/ChatServer.java @@ -87,7 +87,7 @@ public class ChatServer implements ConnectionObserver, ConnectionStateObserver { this.connected = newState == ConnectionState.CONNECTED || newState == ConnectionState.IDLE || newState == ConnectionState.WORKING; synchronized (this) { - this.notifyAll(); + notifyAll(); } } } diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java index e39916c48..26bcf14e8 100644 --- a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java @@ -75,9 +75,9 @@ public class ConsoleEndpoint implements CommunicationEndpoint { this.connection.notifyStateChange(ConnectionState.IDLE, ConnectionState.IDLE.toString()); } } - + @Override public void simulate(IoMessage message) throws Exception { - this.send(message); + send(message); } } diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index e6f3e02e1..5acc85e09 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -584,7 +584,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { @Override public void simulate(IoMessage message) throws Exception { - this.send(message); + send(message); } @Override diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java index 74ccbdc0a..df7173e46 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java @@ -63,7 +63,7 @@ public class SocketEndpointConstants { * Default is to connect on start of the connection */ public static final boolean CONNECT_ON_START = true; - + /** * Default is to not close after sending */ diff --git a/src/main/java/ch/eitchnet/fileserver/FileClient.java b/src/main/java/ch/eitchnet/fileserver/FileClient.java index 3fc356054..0924c8689 100644 --- a/src/main/java/ch/eitchnet/fileserver/FileClient.java +++ b/src/main/java/ch/eitchnet/fileserver/FileClient.java @@ -48,8 +48,8 @@ public interface FileClient { 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 + * 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 * diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/src/main/java/ch/eitchnet/utils/StringMatchMode.java index ebea91029..d096a0c53 100644 --- a/src/main/java/ch/eitchnet/utils/StringMatchMode.java +++ b/src/main/java/ch/eitchnet/utils/StringMatchMode.java @@ -51,13 +51,13 @@ public enum StringMatchMode { 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 (!this.isEquals() && !this.isCaseSensitve()) + if (!isEquals() && !isCaseSensitve()) return value1.toLowerCase().contains(value2.toLowerCase()); - if (!this.isCaseSensitve()) + if (!isCaseSensitve()) return value1.toLowerCase().equals(value2.toLowerCase()); - if (!this.isEquals()) + if (!isEquals()) return value1.contains(value2); return value1.equals(value2); diff --git a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java index 9a7e41bc0..c7090924f 100644 --- a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java +++ b/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java @@ -50,7 +50,7 @@ public class ConsoleEndpointTest extends AbstractEndpointTest { public void testConsoleEndpoint() throws InterruptedException { this.connection.start(); - + CommandKey key = CommandKey.key(CONNECTION_ID, "logger"); //$NON-NLS-1$ TestIoMessage msg = createTestMessage(key, CONNECTION_ID); diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java index 70b6c98f7..e1126d25b 100644 --- a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java +++ b/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java @@ -100,9 +100,9 @@ public class ObjectFilterTest { try { filter.add(myObj); - fail("Should have failed adding twice!"); + fail("Should have failed adding twice!"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid + after +", e.getMessage()); + assertEquals("Stale State exception: Invalid + after +", e.getMessage()); } testAssertions(filter, 1, 1, 1, 0, 0); @@ -118,9 +118,9 @@ public class ObjectFilterTest { try { filter.remove(myObj); - fail("Should have failed removing twice!"); + fail("Should have failed removing twice!"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid - after -", e.getMessage()); + assertEquals("Stale State exception: Invalid - after -", e.getMessage()); } testAssertions(filter, 1, 1, 0, 0, 1); @@ -158,9 +158,9 @@ public class ObjectFilterTest { try { filter.add(myObj); - fail("Should have failed add after modify"); + fail("Should have failed add after modify"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid + after +=", e.getMessage()); + assertEquals("Stale State exception: Invalid + after +=", e.getMessage()); } testAssertions(filter, 1, 1, 0, 1, 0); @@ -186,9 +186,9 @@ public class ObjectFilterTest { try { filter.update(myObj); - fail("Should have failed modify after remove"); + fail("Should have failed modify after remove"); } catch (RuntimeException e) { - assertEquals("Stale State exception: Invalid += after -", e.getMessage()); + assertEquals("Stale State exception: Invalid += after -", e.getMessage()); } testAssertions(filter, 1, 1, 0, 0, 1); From 54092c87d4a5e3eaf0c4cae894367cfafaecad15 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 19 Sep 2014 21:12:07 +0200 Subject: [PATCH 118/180] [Minor] set parent version to 1.0.0-SNAPSHOT --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4b32d8547..0b2e5780e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,11 @@ ch.eitchnet ch.eitchnet.parent - 1.0.0 + 1.0.0-SNAPSHOT ../ch.eitchnet.parent/pom.xml ch.eitchnet.utils - 1.0.0-SNAPSHOT jar ch.eitchnet.utils These utils contain project independent helper classes and utilities for reuse From afcb7c41fc57548f76857a4dd2b868ca0d5b042b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 20 Sep 2014 00:35:00 +0200 Subject: [PATCH 119/180] [Project] clean up --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0b2e5780e..d784a611d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - + + 4.0.0 From 5a2b62bd19f4f6132d0d075ec0db03d17e2fcf79 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 26 Sep 2014 17:18:35 +0200 Subject: [PATCH 120/180] [Minor] changed error message on failed to parse of ISO8601 --- src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java index 327e67b2c..9f66894a1 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java @@ -291,7 +291,7 @@ public class ISO8601 implements DateFormat { return cal.getTime(); } - String msg = "Input string " + s + " cannot be parsed to date."; + String msg = "Input string '" + s + "' cannot be parsed to date."; throw new IllegalArgumentException(msg); } } From 906d24d02bbb4c668bd6c97338c1c813d36cda70 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 4 Oct 2014 23:56:23 +0200 Subject: [PATCH 121/180] [New] Extended FormatFactory to have parse*()-methods --- .../eitchnet/utils/iso8601/FormatFactory.java | 40 ++ .../utils/iso8601/ISO8601Duration.java | 528 +++++++++--------- .../utils/iso8601/ISO8601FormatFactory.java | 198 ++++--- 3 files changed, 413 insertions(+), 353 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java index 6dbf336f2..e728ebe38 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java @@ -70,6 +70,16 @@ public interface FormatFactory { */ public String formatDate(Date date); + /** + * Formats a long as date using {@link #getDateFormat()} + * + * @param date + * the date to format to string + * + * @return String representation of the date + */ + public String formatDate(long date); + /** * Formats a duration using {@link #getDateFormat()} * @@ -99,4 +109,34 @@ public interface FormatFactory { * @return the floating point formatted as a string */ public String formatFloat(double value); + + /** + * Parses a date using {@link #getDateFormat()} + * + * @param date + * the string to parse to date + * + * @return the date + */ + public Date parseDate(String date); + + /** + * Parses a duration using {@link #getDateFormat()} + * + * @param duration + * the string to parse to duration + * + * @return the duration + */ + public long parseDuration(String duration); + + /** + * Parses a work time duration using {@link #getDateFormat()} + * + * @param worktime + * the string duration to parse to work time + * + * @return the work time + */ + public long parseWorktime(String worktime); } \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java index 9d4ae3320..67c147e5c 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -1,264 +1,264 @@ -/* - * 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.iso8601; - -/** - *

    - * Duration is defined as a duration of time, as specified in ISO 8601, Section 5.5.3.2. Its lexical representation is - * the ISO 8601 extended format: PnYnMnDnTnHnMnS - *

    - *
      - *
    • The "P" (period) is required
    • - *
    • "n" represents a positive number
    • - *
    • years is (Y)
    • - *
    • months is (M)
    • - *
    • days is (D)
    • - *
    • time separator is (T), required if any lower terms are given
    • - *
    • hours is (H)
    • - *
    • minutes is (M)
    • - *
    • seconds is (S)
    • - *
    - *

    - * An optional preceding minus sign ("-") is also allowed to indicate a negative duration. If the sign is omitted then a - * positive duration is assumed. For example: is a 2 hour, 5 minute, and - * 2.37 second duration - *

    - *

    - * Remark: since a duration of a day may be measured in hours may vary from 23 an 25 a duration day unit doesn't - * have a meaning, if we do not know either the start or the end, we restrict ourself to measure a duration in hours, - * minutes and seconds - *

    - * - * @author Martin Smock - * @author Michael Gatto (reimplementation using enum) - */ -@SuppressWarnings("nls") -public class ISO8601Duration implements DurationFormat { - - /** - * The time representations available, as enum, with the associated millis. - * - * @author gattom - */ - public enum TimeDuration { - SECOND(1000, 'S'), MINUTE(60 * SECOND.duration(), 'M'), HOUR(60 * MINUTE.duration(), 'H'), DAY(24 * HOUR - .duration(), 'D'), WEEK(7 * DAY.duration(), 'W'), MONTH(30 * DAY.duration(), 'M'), YEAR(12 * MONTH - .duration(), 'Y'); - - final long millis; - final char isoChar; - - TimeDuration(long milli, char isorep) { - this.millis = milli; - this.isoChar = isorep; - } - - public long duration() { - return this.millis; - } - - public static TimeDuration getTimeDurationFor(String isostring, int unitIndex) { - char duration = isostring.charAt(unitIndex); - switch (duration) { - case 'S': - if (isostring.substring(0, unitIndex).contains("T")) { - return SECOND; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); - case 'H': - if (isostring.substring(0, unitIndex).contains("T")) { - return HOUR; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); - case 'D': - return DAY; - case 'W': - return WEEK; - case 'Y': - return YEAR; - case 'M': - if (isostring.substring(0, unitIndex).contains("T")) { - return MINUTE; - } else { - return MONTH; - } - default: - throw new NumberFormatException(duration + " is not a valid unit of time in ISO8601"); - } - } - } - - /** - * check if c is a number char including the decimal decimal dot (.) - * - * @param c - * the character to check - * @return boolean return true if the given char is a number or a decimal dot (.), false otherwise - */ - private static boolean isNumber(char c) { - - boolean isNumber = Character.isDigit(c) || (c == '.'); - return isNumber; - - } - - /** - * Parses the given string to a pseudo ISO 8601 duration - * - * @param s - * the string to be parsed to a duration which must be coded as a ISO8601 value - * @return long the time value which represents the duration - */ - @Override - public long parse(String s) { - - long newResult = 0; - - // throw exception, if the string is not of length > 2 - if (s.length() < 3) - throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); - - char p = s.charAt(0); - - // the first char must be a P for ISO8601 duration - if (p != 'P') - throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); - - int newposition = 1; - do { - if (s.charAt(newposition) == 'T') { - // skip the separator specifying where the time starts. - newposition++; - } - - // read the string representing the numeric value - String val = parseNumber(newposition, s); - double numVal = Double.parseDouble(val); - newposition += val.length(); - - // get the time unit - TimeDuration unit = TimeDuration.getTimeDurationFor(s, newposition); - - // skip the time duration character - newposition++; - - // increment the value. - newResult += unit.duration() * numVal; - - } while (newposition < s.length()); - - return newResult; - } - - /** - * Return the substring of s starting at index i (in s) that contains a numeric string. - * - * @param index - * The start index in string s - * @param s - * The string to analyze - * @return the substring containing the numeric portion of s starting at index i. - */ - private String parseNumber(int index, String s) { - int i = index; - int start = i; - while (i < s.length()) { - if (!isNumber(s.charAt(i))) - break; - i++; - } - String substring = s.substring(start, i); - return substring; - } - - /** - * Format the given time duration unit into the string buffer. This function displays the given duration in units of - * the given unit, and returns the remainder. - *

    - * Thus, a duration of 86401000 (one day and one second) will add the representation of one day if unit is DAY (1D) - * and return 1000 as the remainder with respect of this unit. If the given unit is HOUR, then this function adds - * 24H to the {@link StringBuilder}, and returns 1000 as the remainder. - * - * @param sb - * The {@link StringBuilder} to add the given duration with the right unit - * @param duration - * The duration to add - * @param unit - * The unit of this duration - * @return The remainder of the given duration, modulo the time unit. - */ - private long formatTimeDuration(StringBuilder sb, long duration, TimeDuration unit) { - - long remainder = duration; - if (unit.equals(TimeDuration.SECOND) || remainder >= unit.duration()) { - - long quantity = remainder / unit.duration(); - remainder = remainder % unit.duration(); - sb.append(quantity); - - if (unit.equals(TimeDuration.SECOND)) { - long millis = remainder; - if (millis == 0) { - // to not have the decimal point - } else if (millis > 99) { - sb.append("." + millis); - } else if (millis > 9) { - sb.append(".0" + millis); - } else { - sb.append(".00" + millis); - } - } - - sb.append(unit.isoChar); - } - - return remainder; - } - - /** - * Formats the given time duration to a pseudo ISO 8601 duration string - * - * @param duration - * @return String the duration formatted as a ISO8601 duration string - */ - @Override - public String format(long duration) { - - // XXX this is a preliminary help to solve the situation where this method sometimes returns P - if (duration < 0l) - throw new RuntimeException("A duration can not be negative!"); - - if (duration == 0l) - return "PT0S"; - - StringBuilder sb = new StringBuilder(); - sb.append('P'); - - long remainder = formatTimeDuration(sb, duration, TimeDuration.YEAR); - remainder = formatTimeDuration(sb, remainder, TimeDuration.MONTH); - remainder = formatTimeDuration(sb, remainder, TimeDuration.DAY); - if (remainder > 0) { - sb.append('T'); - remainder = formatTimeDuration(sb, remainder, TimeDuration.HOUR); - remainder = formatTimeDuration(sb, remainder, TimeDuration.MINUTE); - remainder = formatTimeDuration(sb, remainder, TimeDuration.SECOND); - } - return sb.toString(); - } - -} +/* + * 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.iso8601; + +/** + *

    + * Duration is defined as a duration of time, as specified in ISO 8601, Section 5.5.3.2. Its lexical representation is + * the ISO 8601 extended format: PnYnMnDnTnHnMnS + *

    + *
      + *
    • The "P" (period) is required
    • + *
    • "n" represents a positive number
    • + *
    • years is (Y)
    • + *
    • months is (M)
    • + *
    • days is (D)
    • + *
    • time separator is (T), required if any lower terms are given
    • + *
    • hours is (H)
    • + *
    • minutes is (M)
    • + *
    • seconds is (S)
    • + *
    + *

    + * An optional preceding minus sign ("-") is also allowed to indicate a negative duration. If the sign is omitted then a + * positive duration is assumed. For example: is a 2 hour, 5 minute, and + * 2.37 second duration + *

    + *

    + * Remark: since a duration of a day may be measured in hours may vary from 23 an 25 a duration day unit doesn't + * have a meaning, if we do not know either the start or the end, we restrict ourself to measure a duration in hours, + * minutes and seconds + *

    + * + * @author Martin Smock + * @author Michael Gatto (reimplementation using enum) + */ +@SuppressWarnings("nls") +public class ISO8601Duration implements DurationFormat { + + /** + * The time representations available, as enum, with the associated millis. + * + * @author gattom + */ + public enum TimeDuration { + SECOND(1000, 'S'), MINUTE(60 * SECOND.duration(), 'M'), HOUR(60 * MINUTE.duration(), 'H'), DAY(24 * HOUR + .duration(), 'D'), WEEK(7 * DAY.duration(), 'W'), MONTH(30 * DAY.duration(), 'M'), YEAR(12 * MONTH + .duration(), 'Y'); + + final long millis; + final char isoChar; + + TimeDuration(long milli, char isorep) { + this.millis = milli; + this.isoChar = isorep; + } + + public long duration() { + return this.millis; + } + + public static TimeDuration getTimeDurationFor(String isostring, int unitIndex) { + char duration = isostring.charAt(unitIndex); + switch (duration) { + case 'S': + if (isostring.substring(0, unitIndex).contains("T")) { + return SECOND; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); + case 'H': + if (isostring.substring(0, unitIndex).contains("T")) { + return HOUR; + } else + throw new NumberFormatException(duration + + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); + case 'D': + return DAY; + case 'W': + return WEEK; + case 'Y': + return YEAR; + case 'M': + if (isostring.substring(0, unitIndex).contains("T")) { + return MINUTE; + } else { + return MONTH; + } + default: + throw new NumberFormatException(duration + " is not a valid unit of time in ISO8601"); + } + } + } + + /** + * check if c is a number char including the decimal decimal dot (.) + * + * @param c + * the character to check + * @return boolean return true if the given char is a number or a decimal dot (.), false otherwise + */ + private static boolean isNumber(char c) { + + boolean isNumber = Character.isDigit(c) || (c == '.'); + return isNumber; + + } + + /** + * Parses the given string to a pseudo ISO 8601 duration + * + * @param s + * the string to be parsed to a duration which must be coded as a ISO8601 value + * @return long the time value which represents the duration + */ + @Override + public long parse(String s) { + + long newResult = 0; + + // throw exception, if the string is not of length > 2 + if (s == null || s.length() < 3) + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); + + char p = s.charAt(0); + + // the first char must be a P for ISO8601 duration + if (p != 'P') + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); + + int newposition = 1; + do { + if (s.charAt(newposition) == 'T') { + // skip the separator specifying where the time starts. + newposition++; + } + + // read the string representing the numeric value + String val = parseNumber(newposition, s); + double numVal = Double.parseDouble(val); + newposition += val.length(); + + // get the time unit + TimeDuration unit = TimeDuration.getTimeDurationFor(s, newposition); + + // skip the time duration character + newposition++; + + // increment the value. + newResult += unit.duration() * numVal; + + } while (newposition < s.length()); + + return newResult; + } + + /** + * Return the substring of s starting at index i (in s) that contains a numeric string. + * + * @param index + * The start index in string s + * @param s + * The string to analyze + * @return the substring containing the numeric portion of s starting at index i. + */ + private String parseNumber(int index, String s) { + int i = index; + int start = i; + while (i < s.length()) { + if (!isNumber(s.charAt(i))) + break; + i++; + } + String substring = s.substring(start, i); + return substring; + } + + /** + * Format the given time duration unit into the string buffer. This function displays the given duration in units of + * the given unit, and returns the remainder. + *

    + * Thus, a duration of 86401000 (one day and one second) will add the representation of one day if unit is DAY (1D) + * and return 1000 as the remainder with respect of this unit. If the given unit is HOUR, then this function adds + * 24H to the {@link StringBuilder}, and returns 1000 as the remainder. + * + * @param sb + * The {@link StringBuilder} to add the given duration with the right unit + * @param duration + * The duration to add + * @param unit + * The unit of this duration + * @return The remainder of the given duration, modulo the time unit. + */ + private long formatTimeDuration(StringBuilder sb, long duration, TimeDuration unit) { + + long remainder = duration; + if (unit.equals(TimeDuration.SECOND) || remainder >= unit.duration()) { + + long quantity = remainder / unit.duration(); + remainder = remainder % unit.duration(); + sb.append(quantity); + + if (unit.equals(TimeDuration.SECOND)) { + long millis = remainder; + if (millis == 0) { + // to not have the decimal point + } else if (millis > 99) { + sb.append("." + millis); + } else if (millis > 9) { + sb.append(".0" + millis); + } else { + sb.append(".00" + millis); + } + } + + sb.append(unit.isoChar); + } + + return remainder; + } + + /** + * Formats the given time duration to a pseudo ISO 8601 duration string + * + * @param duration + * @return String the duration formatted as a ISO8601 duration string + */ + @Override + public String format(long duration) { + + // XXX this is a preliminary help to solve the situation where this method sometimes returns P + if (duration < 0l) + throw new RuntimeException("A duration can not be negative!"); + + if (duration == 0l) + return "PT0S"; + + StringBuilder sb = new StringBuilder(); + sb.append('P'); + + long remainder = formatTimeDuration(sb, duration, TimeDuration.YEAR); + remainder = formatTimeDuration(sb, remainder, TimeDuration.MONTH); + remainder = formatTimeDuration(sb, remainder, TimeDuration.DAY); + if (remainder > 0) { + sb.append('T'); + remainder = formatTimeDuration(sb, remainder, TimeDuration.HOUR); + remainder = formatTimeDuration(sb, remainder, TimeDuration.MINUTE); + remainder = formatTimeDuration(sb, remainder, TimeDuration.SECOND); + } + return sb.toString(); + } + +} diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java index c8ea878c7..2cc57f670 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java @@ -1,89 +1,109 @@ -/* - * 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.iso8601; - -import java.util.Date; - -import ch.eitchnet.utils.helper.MathHelper; - -/** - * Default factory for date formats used for serialization. - * - * @author Martin Smock - */ -public class ISO8601FormatFactory implements FormatFactory { - - private static ISO8601FormatFactory instance = new ISO8601FormatFactory(); - - /** - * the singleton constructor - */ - private ISO8601FormatFactory() { - // singleton - } - - /** - * @return the instance - */ - public static ISO8601FormatFactory getInstance() { - return instance; - } - - @Override - public ISO8601 getDateFormat() { - return new ISO8601(); - } - - @Override - public ISO8601Duration getDurationFormat() { - return new ISO8601Duration(); - } - - @Override - public ISO8601Worktime getWorktimeFormat() { - return new ISO8601Worktime(); - } - - @Override - public ISO8601 getXmlDateFormat() { - return new ISO8601(); - } - - @Override - public ISO8601Duration getXmlDurationFormat() { - return new ISO8601Duration(); - } - - @Override - public String formatDate(Date date) { - return getDateFormat().format(date); - } - - @Override - public String formatDuration(long duration) { - return getDurationFormat().format(duration); - } - - @Override - public String formatWorktime(long worktime) { - return getDurationFormat().format(worktime); - } - - @Override - public String formatFloat(double value) { - return Double.toString(MathHelper.toPrecision(value)); - } -} +/* + * 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.iso8601; + +import java.util.Date; + +import ch.eitchnet.utils.helper.MathHelper; + +/** + * Default factory for date formats used for serialization. + * + * @author Martin Smock + */ +public class ISO8601FormatFactory implements FormatFactory { + + private static ISO8601FormatFactory instance = new ISO8601FormatFactory(); + + /** + * the singleton constructor + */ + private ISO8601FormatFactory() { + // singleton + } + + /** + * @return the instance + */ + public static ISO8601FormatFactory getInstance() { + return instance; + } + + @Override + public ISO8601 getDateFormat() { + return new ISO8601(); + } + + @Override + public ISO8601Duration getDurationFormat() { + return new ISO8601Duration(); + } + + @Override + public ISO8601Worktime getWorktimeFormat() { + return new ISO8601Worktime(); + } + + @Override + public ISO8601 getXmlDateFormat() { + return new ISO8601(); + } + + @Override + public ISO8601Duration getXmlDurationFormat() { + return new ISO8601Duration(); + } + + @Override + public String formatDate(Date date) { + return getDateFormat().format(date); + } + + @Override + public String formatDate(long date) { + return getDateFormat().format(date); + } + + @Override + public String formatDuration(long duration) { + return getDurationFormat().format(duration); + } + + @Override + public String formatWorktime(long worktime) { + return getDurationFormat().format(worktime); + } + + @Override + public String formatFloat(double value) { + return Double.toString(MathHelper.toPrecision(value)); + } + + @Override + public Date parseDate(String date) { + return getDateFormat().parse(date); + } + + @Override + public long parseDuration(String duration) { + return getDurationFormat().parse(duration); + } + + @Override + public long parseWorktime(String worktime) { + return getDurationFormat().parse(worktime); + } +} From 84a24feb894568b6e4820f11819d4cafa603e0ac Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 14 Nov 2014 00:04:07 +0100 Subject: [PATCH 122/180] [New] Added package 'db' with utils for schema CUD DbSchemaVersionCheck is used in combination with DbConnectionInfo to create schemas from SQL scripts. Using DbConnectionCheck.checkSchemaVersion() schemas are created or migrated. There is also functionality to drop schemas --- .../ch/eitchnet/db/DbConnectionCheck.java | 78 +++++ .../java/ch/eitchnet/db/DbConnectionInfo.java | 132 ++++++++ src/main/java/ch/eitchnet/db/DbConstants.java | 32 ++ .../java/ch/eitchnet/db/DbDriverLoader.java | 49 +++ src/main/java/ch/eitchnet/db/DbException.java | 39 +++ .../java/ch/eitchnet/db/DbMigrationState.java | 23 ++ .../ch/eitchnet/db/DbSchemaVersionCheck.java | 306 ++++++++++++++++++ 7 files changed, 659 insertions(+) create mode 100644 src/main/java/ch/eitchnet/db/DbConnectionCheck.java create mode 100644 src/main/java/ch/eitchnet/db/DbConnectionInfo.java create mode 100644 src/main/java/ch/eitchnet/db/DbConstants.java create mode 100644 src/main/java/ch/eitchnet/db/DbDriverLoader.java create mode 100644 src/main/java/ch/eitchnet/db/DbException.java create mode 100644 src/main/java/ch/eitchnet/db/DbMigrationState.java create mode 100644 src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java diff --git a/src/main/java/ch/eitchnet/db/DbConnectionCheck.java b/src/main/java/ch/eitchnet/db/DbConnectionCheck.java new file mode 100644 index 000000000..9177757ee --- /dev/null +++ b/src/main/java/ch/eitchnet/db/DbConnectionCheck.java @@ -0,0 +1,78 @@ +/* + * 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.DriverManager; +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 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 connetionInfoMap; + + /** + * @param connetionInfoMap + */ + public DbConnectionCheck(Map connetionInfoMap) { + this.connetionInfoMap = connetionInfoMap; + } + + /** + * Checks the connectivity to each of the configured {@link DbConnectionInfo} + * + * @throws DbException + */ + public void checkConnections() throws DbException { + Collection values = this.connetionInfoMap.values(); + for (DbConnectionInfo connectionInfo : values) { + + String url = connectionInfo.getUrl(); + String username = connectionInfo.getUsername(); + String password = connectionInfo.getPassword(); + + logger.info("Checking connection " + username + "@" + url); + + try (Connection con = DriverManager.getConnection(url, username, password); + 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 URL {0} due to: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, url, e.getMessage()); + throw new DbException(msg, e); + } + } + + logger.info("All connections OK"); + } +} diff --git a/src/main/java/ch/eitchnet/db/DbConnectionInfo.java b/src/main/java/ch/eitchnet/db/DbConnectionInfo.java new file mode 100644 index 000000000..391bb8d3f --- /dev/null +++ b/src/main/java/ch/eitchnet/db/DbConnectionInfo.java @@ -0,0 +1,132 @@ +/* + * 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.DriverManager; +import java.sql.SQLException; +import java.text.MessageFormat; + +import ch.eitchnet.utils.dbc.DBC; + +/** + * @author Robert von Burg + */ +public class DbConnectionInfo { + + private String realm; + private String url; + private String username; + private String password; + + public DbConnectionInfo(String realm, String url) { + DBC.PRE.assertNotEmpty("Realm must be set!", realm); //$NON-NLS-1$ + DBC.PRE.assertNotEmpty("Url must be set!", url); //$NON-NLS-1$ + this.realm = realm; + this.url = url; + } + + /** + * @return the realm + */ + public String getRealm() { + return this.realm; + } + + /** + * @param realm + * the realm to set + */ + public void setRealm(String realm) { + this.realm = realm; + } + + /** + * @return the url + */ + public String getUrl() { + return this.url; + } + + /** + * @param url + * the url to set + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * @return the username + */ + public String getUsername() { + return this.username; + } + + /** + * @param username + * the username to set + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * @return the password + */ + public String getPassword() { + return this.password; + } + + /** + * @param password + * the password to set + */ + public void setPassword(String password) { + this.password = password; + } + + @SuppressWarnings("nls") + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("DbConnectionInfo [realm="); + builder.append(this.realm); + builder.append(", url="); + builder.append(this.url); + builder.append(", username="); + builder.append(this.username); + builder.append(", password=***"); + builder.append("]"); + return builder.toString(); + } + + /** + * @return a {@link Connection} + * + * @throws DbException + */ + public Connection openConnection() throws DbException { + try { + Connection connection = DriverManager.getConnection(this.url, this.username, this.password); + connection.setAutoCommit(false); + return connection; + } catch (SQLException e) { + String msg = MessageFormat.format("Failed to get a connection for {0} due to {1}", this, e.getMessage()); //$NON-NLS-1$ + throw new DbException(msg, e); + } + } +} diff --git a/src/main/java/ch/eitchnet/db/DbConstants.java b/src/main/java/ch/eitchnet/db/DbConstants.java new file mode 100644 index 000000000..ceee7a85e --- /dev/null +++ b/src/main/java/ch/eitchnet/db/DbConstants.java @@ -0,0 +1,32 @@ +/* + * 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_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/src/main/java/ch/eitchnet/db/DbDriverLoader.java b/src/main/java/ch/eitchnet/db/DbDriverLoader.java new file mode 100644 index 000000000..a824cce1a --- /dev/null +++ b/src/main/java/ch/eitchnet/db/DbDriverLoader.java @@ -0,0 +1,49 @@ +/* + * 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.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.text.MessageFormat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Robert von Burg + */ +public class DbDriverLoader { + + private static final Logger logger = LoggerFactory.getLogger(DbDriverLoader.class); + + public static void loadDriverForConnection(DbConnectionInfo connectionInfo) throws DbException { + Driver driver; + try { + driver = DriverManager.getDriver(connectionInfo.getUrl()); + } catch (SQLException e) { + String msg = "Failed to load DB driver for URL {0} due to: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, connectionInfo.getUrl(), e.getMessage()); + throw new DbException(msg, e); + } + + String compliant = driver.jdbcCompliant() ? "" : "non"; //$NON-NLS-1$ //$NON-NLS-2$ + String msg = "Realm {0}: Using {1} JDBC compliant Driver {2}.{3}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, connectionInfo.getRealm(), compliant, driver.getMajorVersion(), + driver.getMinorVersion()); + logger.info(msg); + } +} diff --git a/src/main/java/ch/eitchnet/db/DbException.java b/src/main/java/ch/eitchnet/db/DbException.java new file mode 100644 index 000000000..f1c6bb942 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/db/DbMigrationState.java b/src/main/java/ch/eitchnet/db/DbMigrationState.java new file mode 100644 index 000000000..f937ef344 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java new file mode 100644 index 000000000..119729988 --- /dev/null +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -0,0 +1,306 @@ +/* + * 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.DriverManager; +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.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 scriptPrefix; + private Class ctxClass; + private boolean allowSchemaCreation; + private boolean allowSchemaMigration; + private boolean allowSchemaDrop; + private Map dbMigrationStates; + + /** + * @param scriptPrefix + * @param ctxClass + * @param allowSchemaCreation + * @param allowSchemaDrop + */ + public DbSchemaVersionCheck(String scriptPrefix, Class ctxClass, boolean allowSchemaCreation, + boolean allowSchemaMigration, boolean allowSchemaDrop) { + + DBC.PRE.assertNotEmpty("scriptPrefix may not be empty!", scriptPrefix); + DBC.PRE.assertNotNull("ctxClass may not be null!", ctxClass); + + this.scriptPrefix = scriptPrefix; + 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 connectionInfoMap) throws DbException { + for (DbConnectionInfo connectionInfo : connectionInfoMap.values()) { + DbMigrationState dbMigrationState = checkSchemaVersion(connectionInfo); + dbMigrationStates.put(connectionInfo.getRealm(), dbMigrationState); + } + } + + /** + * Returns true if the schema existed or was only migrated, false if the schema was created + * + * @param connectionInfo + * + * @return true if the schema existed or was only migrated, false if the schema was created + * + * @throws DbException + */ + public DbMigrationState checkSchemaVersion(DbConnectionInfo connectionInfo) throws DbException { + String realm = connectionInfo.getRealm(); + String url = connectionInfo.getUrl(); + String username = connectionInfo.getUsername(); + String password = connectionInfo.getPassword(); + + logger.info(MessageFormat.format("[{0}] Checking Schema version for: {1}@{2}", realm, username, url)); + + DbMigrationState migrationType; + + try (Connection con = DriverManager.getConnection(url, username, password); + Statement st = con.createStatement();) { + + String expectedDbVersion = getExpectedDbVersion(this.scriptPrefix, this.ctxClass); + + // first see if we have any schema + String msg = "select table_schema, table_name, table_type from information_schema.tables where table_name=''{0}'';"; + String checkSchemaExistsSql = MessageFormat.format(msg, PROP_DB_VERSION); + try (ResultSet rs = st.executeQuery(checkSchemaExistsSql)) { + if (!rs.next()) { + migrationType = createSchema(realm, expectedDbVersion, st); + } else { + migrationType = checkCurrentVersion(realm, st, expectedDbVersion); + } + } + + return migrationType; + + } catch (SQLException e) { + String msg = "Failed to open DB connection to URL {0} due to: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, url, e.getMessage()); + throw new DbException(msg, e); + } + } + + /** + * @param realm + * @param st + * @param expectedDbVersion + * + * @return + * + * @throws SQLException + * @throws DbException + */ + public DbMigrationState checkCurrentVersion(String realm, Statement st, String expectedDbVersion) + throws SQLException, DbException { + try (ResultSet rs = st.executeQuery("select id, version from db_version order by id desc;")) { + if (!rs.next()) { + return createSchema(realm, expectedDbVersion, st); + } else { + String currentVersion = rs.getString(2); + if (expectedDbVersion.equals(currentVersion)) { + String msg = "[{0}] Schema version {1} is the current version. No changes needed."; + msg = MessageFormat.format(msg, realm, currentVersion); + logger.info(msg); + return DbMigrationState.NOTHING; + } else { + String msg = "[{0}] Schema version is not current. Need to upgrade from {1} to {2}"; + msg = MessageFormat.format(msg, realm, currentVersion, expectedDbVersion); + logger.warn(msg); + return migrateSchema(realm, expectedDbVersion, st); + } + } + } + } + + public static String getExpectedDbVersion(String prefix, Class ctxClass) throws DbException { + Properties dbVersionProps = new Properties(); + + String dbVersionPropFile = MessageFormat.format(RESOURCE_DB_VERSION, prefix); + + 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 dbVersion; + } + + /** + * @param scriptPrefix + * @param classLoader + * @param dbVersion + * @param type + * + * @return + * + * @throws DbException + */ + public static String getSql(String scriptPrefix, Class ctxClass, String 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 DbMigrationState createSchema(String realm, String dbVersion, Statement st) throws DbException { + + if (!this.allowSchemaCreation) { + String msg = "[{0}] No schema exists, or is not valid. Schema generation is disabled, thus can not continue!"; + msg = MessageFormat.format(msg, realm); + throw new DbException(msg); + } + + logger.info(MessageFormat.format("[{0}] Creating initial schema...", realm)); + + String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "initial"); + try { + 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}] Successfully created schema for version {1}", realm, dbVersion)); + return DbMigrationState.CREATED; + } + + /** + * @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(String realm, String dbVersion, Statement st) throws DbException { + + if (!this.allowSchemaDrop) { + String msg = "[{0}] Dropping Schema is disabled, but is required to upgrade current schema..."; + msg = MessageFormat.format(msg, realm); + throw new DbException(msg); + } + + logger.info(MessageFormat.format("[{0}] Dropping existing schema...", realm)); + + String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "drop"); + try { + 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); + } + } + + /** + * 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 + * @param st + * the open database {@link Statement} to which the SQL statements will be written + * + * @return true if the schema was recreated, false if it was simply migrated + * + * @throws DbException + */ + public DbMigrationState migrateSchema(String realm, String dbVersion, Statement st) throws DbException { + + if (!this.allowSchemaMigration) { + String msg = "[{0}] Schema is not valid. Schema migration is disabled, thus can not continue!"; + msg = MessageFormat.format(msg, realm); + throw new DbException(msg); + } + + logger.info(MessageFormat.format("[{0}] Migrating schema...", realm)); + + String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "migration"); + try { + 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}] Successfully migrated schema to version {1}", realm, dbVersion)); + return DbMigrationState.MIGRATED; + } +} From 3f6f9950f856dd995ca1d3581aabe30ba3db910e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 28 Nov 2014 22:20:50 +0100 Subject: [PATCH 123/180] [New] Created new exception formatting method when only message needed --- .../eitchnet/utils/helper/StringHelper.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 526f63f4a..c6306e61c 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -578,6 +578,36 @@ public class StringHelper { 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) { + StringBuilder sb = new StringBuilder(); + sb.append(t.getMessage()); + sb.append("\n"); + + appendCause(sb, t); + return sb.toString(); + } + + private static void appendCause(StringBuilder sb, Throwable e) { + Throwable cause = e.getCause(); + if (cause == null) + return; + + sb.append("cause:\n"); + sb.append(cause.getMessage()); + sb.append("\n"); + + if (cause.getCause() != null) + appendCause(sb, cause.getCause()); + } + /** * Simply returns true if the value is null, or empty * From 07b574dba13ae7ca1000737dc799f6fd3885bd35 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 9 Jan 2015 12:32:18 +0100 Subject: [PATCH 124/180] [Major] refactored DbSchemaVersionCheck and added version parsing - Now we can use Version to see if current version is greater than - Fixed bug where version checking didn't check for app - TODO is still open to handle migration with intermediary steps --- .../ch/eitchnet/db/DbSchemaVersionCheck.java | 216 +++++--- src/main/java/ch/eitchnet/utils/Version.java | 491 ++++++++++++++++++ .../java/ch/eitchnet/utils/VersionTest.java | 78 +++ 3 files changed, 701 insertions(+), 84 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/Version.java create mode 100644 src/test/java/ch/eitchnet/utils/VersionTest.java diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index 119729988..08935f24f 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -33,6 +34,7 @@ import java.util.Properties; 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; @@ -43,7 +45,7 @@ import ch.eitchnet.utils.helper.FileHelper; public class DbSchemaVersionCheck { private static final Logger logger = LoggerFactory.getLogger(DbSchemaVersionCheck.class); - private String scriptPrefix; + private String app; private Class ctxClass; private boolean allowSchemaCreation; private boolean allowSchemaMigration; @@ -51,18 +53,18 @@ public class DbSchemaVersionCheck { private Map dbMigrationStates; /** - * @param scriptPrefix + * @param app * @param ctxClass * @param allowSchemaCreation * @param allowSchemaDrop */ - public DbSchemaVersionCheck(String scriptPrefix, Class ctxClass, boolean allowSchemaCreation, + public DbSchemaVersionCheck(String app, Class ctxClass, boolean allowSchemaCreation, boolean allowSchemaMigration, boolean allowSchemaDrop) { - DBC.PRE.assertNotEmpty("scriptPrefix may not be empty!", scriptPrefix); + DBC.PRE.assertNotEmpty("app may not be empty!", app); DBC.PRE.assertNotNull("ctxClass may not be null!", ctxClass); - this.scriptPrefix = scriptPrefix; + this.app = app; this.ctxClass = ctxClass; this.allowSchemaCreation = allowSchemaCreation; this.allowSchemaMigration = allowSchemaMigration; @@ -101,22 +103,27 @@ public class DbSchemaVersionCheck { logger.info(MessageFormat.format("[{0}] Checking Schema version for: {1}@{2}", realm, username, url)); - DbMigrationState migrationType; + Version expectedDbVersion = getExpectedDbVersion(this.app, this.ctxClass); - try (Connection con = DriverManager.getConnection(url, username, password); - Statement st = con.createStatement();) { + try (Connection con = DriverManager.getConnection(url, username, password)) { - String expectedDbVersion = getExpectedDbVersion(this.scriptPrefix, this.ctxClass); + // get current version + Version currentVersion = getCurrentVersion(con, this.app); + DbMigrationState migrationType = detectMigrationState(realm, expectedDbVersion, currentVersion); - // first see if we have any schema - String msg = "select table_schema, table_name, table_type from information_schema.tables where table_name=''{0}'';"; - String checkSchemaExistsSql = MessageFormat.format(msg, PROP_DB_VERSION); - try (ResultSet rs = st.executeQuery(checkSchemaExistsSql)) { - if (!rs.next()) { - migrationType = createSchema(realm, expectedDbVersion, st); - } else { - migrationType = checkCurrentVersion(realm, st, expectedDbVersion); - } + 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; } return migrationType; @@ -128,9 +135,39 @@ public class DbSchemaVersionCheck { } } + /** + * @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(0, 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(0, app); + ResultSet rs = st.executeQuery(); + if (rs.next()) + currentVersion = Version.valueOf(rs.getString(2)); + } + + return currentVersion; + } + /** * @param realm - * @param st * @param expectedDbVersion * * @return @@ -138,32 +175,43 @@ public class DbSchemaVersionCheck { * @throws SQLException * @throws DbException */ - public DbMigrationState checkCurrentVersion(String realm, Statement st, String expectedDbVersion) + public DbMigrationState detectMigrationState(String realm, Version expectedDbVersion, Version currentVersion) throws SQLException, DbException { - try (ResultSet rs = st.executeQuery("select id, version from db_version order by id desc;")) { - if (!rs.next()) { - return createSchema(realm, expectedDbVersion, st); - } else { - String currentVersion = rs.getString(2); - if (expectedDbVersion.equals(currentVersion)) { - String msg = "[{0}] Schema version {1} is the current version. No changes needed."; - msg = MessageFormat.format(msg, realm, currentVersion); - logger.info(msg); - return DbMigrationState.NOTHING; - } else { - String msg = "[{0}] Schema version is not current. Need to upgrade from {1} to {2}"; - msg = MessageFormat.format(msg, realm, currentVersion, expectedDbVersion); - logger.warn(msg); - return migrateSchema(realm, expectedDbVersion, st); - } - } + + // 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}] Schema version {1} is the current version. No changes needed."; + msg = MessageFormat.format(msg, realm, currentVersion); + logger.info(msg); + return DbMigrationState.NOTHING; + } else if (compare > 0) { + String msg = "[{0}] Schema version is not current. Need to upgrade from {1} to {2}"; + msg = MessageFormat.format(msg, realm, currentVersion, expectedDbVersion); + logger.warn(msg); + return DbMigrationState.MIGRATED; } + + throw new DbException("Current version " + currentVersion + " is later than expected version " + + expectedDbVersion); } - public static String getExpectedDbVersion(String prefix, Class ctxClass) throws DbException { + /** + * @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, prefix); + String dbVersionPropFile = MessageFormat.format(RESOURCE_DB_VERSION, app); try (InputStream stream = ctxClass.getResourceAsStream(dbVersionPropFile);) { DBC.PRE.assertNotNull( @@ -177,7 +225,8 @@ public class DbSchemaVersionCheck { 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 dbVersion; + + return Version.valueOf(dbVersion); } /** @@ -190,7 +239,7 @@ public class DbSchemaVersionCheck { * * @throws DbException */ - public static String getSql(String scriptPrefix, Class ctxClass, String dbVersion, String type) + 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);) { @@ -216,7 +265,7 @@ public class DbSchemaVersionCheck { * * @throws DbException */ - public DbMigrationState createSchema(String realm, String dbVersion, Statement st) throws DbException { + public DbMigrationState createSchema(Connection con, String realm, Version dbVersion) throws DbException { if (!this.allowSchemaCreation) { String msg = "[{0}] No schema exists, or is not valid. Schema generation is disabled, thus can not continue!"; @@ -226,8 +275,9 @@ public class DbSchemaVersionCheck { logger.info(MessageFormat.format("[{0}] Creating initial schema...", realm)); - String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "initial"); - try { + 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); @@ -238,6 +288,41 @@ public class DbSchemaVersionCheck { return DbMigrationState.CREATED; } + /** + * 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 DbMigrationState migrateSchema(Connection con, String realm, Version dbVersion) throws DbException { + + if (!this.allowSchemaMigration) { + String msg = "[{0}] Schema is not valid. Schema migration is disabled, thus can not continue!"; + msg = MessageFormat.format(msg, realm); + throw new DbException(msg); + } + + logger.info(MessageFormat.format("[{0}] Migrating schema...", 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}] Successfully migrated schema to version {1}", realm, dbVersion)); + return DbMigrationState.MIGRATED; + } + /** * @param realm * the realm for which the schema must be dropped (a {@link DbConnectionInfo} must exist for it) @@ -248,7 +333,7 @@ public class DbSchemaVersionCheck { * * @throws DbException */ - public void dropSchema(String realm, String dbVersion, Statement st) throws DbException { + public void dropSchema(Connection con, String realm, Version dbVersion) throws DbException { if (!this.allowSchemaDrop) { String msg = "[{0}] Dropping Schema is disabled, but is required to upgrade current schema..."; @@ -258,49 +343,12 @@ public class DbSchemaVersionCheck { logger.info(MessageFormat.format("[{0}] Dropping existing schema...", realm)); - String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "drop"); - try { + 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); } } - - /** - * 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 - * @param st - * the open database {@link Statement} to which the SQL statements will be written - * - * @return true if the schema was recreated, false if it was simply migrated - * - * @throws DbException - */ - public DbMigrationState migrateSchema(String realm, String dbVersion, Statement st) throws DbException { - - if (!this.allowSchemaMigration) { - String msg = "[{0}] Schema is not valid. Schema migration is disabled, thus can not continue!"; - msg = MessageFormat.format(msg, realm); - throw new DbException(msg); - } - - logger.info(MessageFormat.format("[{0}] Migrating schema...", realm)); - - String sql = getSql(this.scriptPrefix, this.ctxClass, dbVersion, "migration"); - try { - 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}] Successfully migrated schema to version {1}", realm, dbVersion)); - return DbMigrationState.MIGRATED; - } } diff --git a/src/main/java/ch/eitchnet/utils/Version.java b/src/main/java/ch/eitchnet/utils/Version.java new file mode 100644 index 000000000..d8abaf340 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/Version.java @@ -0,0 +1,491 @@ +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. + *

      + *
    1. Major version. A non-negative integer.
    2. + *
    3. Minor version. A non-negative integer.
    4. + *
    5. Micro version. A non-negative integer.
    6. + *
    7. Qualifier. A text string. See {@code Version(String)} for the format of the qualifier string.
    8. + *
    + * + * 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: + * + *

    + * version ::= major('.'minor('.'micro('.'qualifier)?)?)?
    + * major ::= digit+
    + * minor ::= digit+
    + * micro ::= digit+
    + * qualifier ::= (alpha|digit|'_'|'-')+
    + * digit ::= [0..9]
    + * alpha ::= [a..zA..Z]
    + * 
    + * + * Note: There must be no whitespace in version. + *

    + * + *

    + * {@code Version} objects are immutable. + *

    + */ +public class Version implements Comparable { + private final int major; + private final int minor; + private final int micro; + private final String qualifier; + + private static final String SEPARATOR = "."; + private static final String OSGI_QUALIFIER_SEPARATOR = "."; + private static final String MAVEN_QUALIFIER_SEPARATOR = "-"; + + private static final String MAVEN_SNAPSHOT_QUALIFIER = "SNAPSHOT"; + private static final String OSGI_SNAPSHOT_QUALIFIER = "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) { + if (qualifier == null) { + qualifier = ""; + } + + this.major = major; + this.minor = minor; + this.micro = micro; + this.qualifier = 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; + } + + version = version.trim(); + if (version.length() == 0) { + return emptyVersion; + } + + return new Version(version); + } + + /** + * 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; + } + + public boolean isOsgiStyle() { + return this.osgiStyle; + } + + /** + * 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) { + return this.versionString; + } + 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; + } else { + return MAVEN_SNAPSHOT_QUALIFIER; + } + } + return 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); + } + + /** + * @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(); + } + + public boolean isSnapshot() { + return MAVEN_SNAPSHOT_QUALIFIER.equals(this.qualifier) || OSGI_SNAPSHOT_QUALIFIER.equals(this.qualifier); + } +} diff --git a/src/test/java/ch/eitchnet/utils/VersionTest.java b/src/test/java/ch/eitchnet/utils/VersionTest.java new file mode 100644 index 000000000..bd3c7304e --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/VersionTest.java @@ -0,0 +1,78 @@ +package ch.eitchnet.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import ch.eitchnet.utils.helper.StringHelper; + +/** + * Tests the {@link Version} class + */ +public class VersionTest { + + @Test + public void shouldParseMajoMinoMicro() { + Version version = Version.valueOf("1.0.2"); + assertEquals(1, version.getMajor()); + assertEquals(0, version.getMinor()); + assertEquals(2, version.getMicro()); + assertEquals(StringHelper.EMPTY, version.getQualifier()); + assertEquals(StringHelper.EMPTY, version.getQualifier()); + + assertEquals("1.0.2", version.toString()); + } + + @Test + public void shouldParseVersion() { + { + Version version = Version.valueOf("7.5.6.1"); + assertEquals(7, version.getMajor()); + assertEquals(5, version.getMinor()); + assertEquals(6, version.getMicro()); + assertEquals("1", version.getQualifier()); + assertTrue(version.isOsgiStyle()); + } + { + Version version = Version.valueOf("7.5.6-1"); + assertEquals(7, version.getMajor()); + assertEquals(5, version.getMinor()); + assertEquals(6, version.getMicro()); + assertEquals("1", version.getQualifier()); + assertFalse(version.isOsgiStyle()); + } + } + + @Test + public void shouldCompareVersions() { + assertEquals(0, Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-1"))); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-2")) < 0); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.0")) < 0); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.1")) < 0); + assertTrue(Version.valueOf("7.5.6-alpha").compareTo(Version.valueOf("7.6.1-beta")) < 0); + assertTrue(Version.valueOf("7.7.0-0").compareTo(Version.valueOf("7.6.99-9")) > 0); + } + + @Test + public void shouldConvertToMajorMinorString() { + assertEquals("7.6", Version.valueOf("7.6.1-0").toMajorAndMinorString()); + } + + @Test + public void shouldKnowAboutBeingFullyQualified() { + assertFalse(Version.valueOf("7").isFullyQualified()); + assertFalse(Version.valueOf("7.6").isFullyQualified()); + assertFalse(Version.valueOf("7.6.1").isFullyQualified()); + assertTrue(Version.valueOf("7.6.1-0").isFullyQualified()); + } + + @Test + public void shouldDealWithEclipseStyleSnapshotQualifier() { + assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toOsgiStyleString(), "7.6.1.qualifier"); + assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toMavenStyleString(), "7.6.1-SNAPSHOT"); + assertEquals(Version.valueOf("7.6.1.qualifier").toOsgiStyleString(), "7.6.1.qualifier"); + assertEquals(Version.valueOf("7.6.1.qualifier").toMavenStyleString(), "7.6.1-SNAPSHOT"); + } +} From fc7c2d6600f0635faea4782ae6242b6d0236e22e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 9 Jan 2015 12:39:23 +0100 Subject: [PATCH 125/180] [Major] refactored DbSchemaVersionCheck and added version parsing - Now we can use Version to see if current version is greater than - Fixed bug where version checking didn't check for app - TODO is still open to handle migration with intermediary steps --- src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index 08935f24f..7ee747190 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -148,7 +148,7 @@ public class DbSchemaVersionCheck { // 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(0, PROP_DB_VERSION); + st.setString(1, PROP_DB_VERSION); if (!st.executeQuery().next()) return null; } @@ -157,7 +157,7 @@ public class DbSchemaVersionCheck { sql = "select id, version from db_version where app = ? order by id desc;"; Version currentVersion = null; try (PreparedStatement st = con.prepareStatement(sql)) { - st.setString(0, app); + st.setString(1, app); ResultSet rs = st.executeQuery(); if (rs.next()) currentVersion = Version.valueOf(rs.getString(2)); From 5a05bc835d76be02c41a54f16d4493155da4669d Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 9 Jan 2015 12:59:45 +0100 Subject: [PATCH 126/180] [Minor] Added logging of realm and app when checking db schema --- .../ch/eitchnet/db/DbSchemaVersionCheck.java | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index 7ee747190..d4c260219 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -101,7 +101,8 @@ public class DbSchemaVersionCheck { String username = connectionInfo.getUsername(); String password = connectionInfo.getPassword(); - logger.info(MessageFormat.format("[{0}] Checking Schema version for: {1}@{2}", realm, username, url)); + logger.info(MessageFormat.format("[{0}:{1}] Checking Schema version for: {2}@{3}", this.app, realm, username, + url)); Version expectedDbVersion = getExpectedDbVersion(this.app, this.ctxClass); @@ -185,19 +186,19 @@ public class DbSchemaVersionCheck { // otherwise parse the version int compare = expectedDbVersion.compareTo(currentVersion); if (compare == 0) { - String msg = "[{0}] Schema version {1} is the current version. No changes needed."; - msg = MessageFormat.format(msg, realm, currentVersion); + 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}] Schema version is not current. Need to upgrade from {1} to {2}"; - msg = MessageFormat.format(msg, realm, currentVersion, expectedDbVersion); + 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("Current version " + currentVersion + " is later than expected version " - + expectedDbVersion); + throw new DbException(MessageFormat.format("[{0}:{1}]Current version {2} is later than expected version {3}", + this.app, realm, currentVersion, expectedDbVersion)); } /** @@ -265,15 +266,15 @@ public class DbSchemaVersionCheck { * * @throws DbException */ - public DbMigrationState createSchema(Connection con, String realm, Version dbVersion) throws DbException { + public void createSchema(Connection con, String realm, Version dbVersion) throws DbException { if (!this.allowSchemaCreation) { - String msg = "[{0}] No schema exists, or is not valid. Schema generation is disabled, thus can not continue!"; - msg = MessageFormat.format(msg, realm); + 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}] Creating initial schema...", realm)); + logger.info(MessageFormat.format("[{0}:{1}] Creating initial schema...", this.app, realm)); String sql = getSql(this.app, this.ctxClass, dbVersion, "initial"); @@ -284,8 +285,8 @@ public class DbSchemaVersionCheck { throw new DbException("Failed to execute schema generation SQL: " + e.getMessage(), e); } - logger.info(MessageFormat.format("[{0}] Successfully created schema for version {1}", realm, dbVersion)); - return DbMigrationState.CREATED; + logger.info(MessageFormat.format("[{0}:{1}] Successfully created schema with version {2}", this.app, realm, + dbVersion)); } /** @@ -301,15 +302,15 @@ public class DbSchemaVersionCheck { * * @throws DbException */ - public DbMigrationState migrateSchema(Connection con, String realm, Version dbVersion) throws DbException { + public void migrateSchema(Connection con, String realm, Version dbVersion) throws DbException { if (!this.allowSchemaMigration) { - String msg = "[{0}] Schema is not valid. Schema migration is disabled, thus can not continue!"; - msg = MessageFormat.format(msg, realm); + 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}] Migrating schema...", realm)); + 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()) { @@ -319,8 +320,8 @@ public class DbSchemaVersionCheck { throw new DbException("Failed to execute schema migration SQL: " + e.getMessage(), e); } - logger.info(MessageFormat.format("[{0}] Successfully migrated schema to version {1}", realm, dbVersion)); - return DbMigrationState.MIGRATED; + logger.info(MessageFormat.format("[{0}:{1}] Successfully migrated schema to version {2}", this.app, realm, + dbVersion)); } /** @@ -336,12 +337,12 @@ public class DbSchemaVersionCheck { public void dropSchema(Connection con, String realm, Version dbVersion) throws DbException { if (!this.allowSchemaDrop) { - String msg = "[{0}] Dropping Schema is disabled, but is required to upgrade current schema..."; - msg = MessageFormat.format(msg, realm); + 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}] Dropping existing schema...", realm)); + 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()) { @@ -350,5 +351,8 @@ public class DbSchemaVersionCheck { 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)); } } From 7c4fdbaed757fa2d82249d5fa09e2db13b082cea Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 15 Jan 2015 09:05:37 +0100 Subject: [PATCH 127/180] [Major] Refactorings in communication package - Renaming of CommunicationConnection.notify to handleNewMessage() - added transient properties map to IoMessage - Fixed bug in updating of observers in CommunicationConnection --- .../CommunicationConnection.java | 20 +- .../ch/eitchnet/communication/IoMessage.java | 52 ++ .../communication/file/FileEndpoint.java | 502 +++++++++--------- .../tcpip/ServerSocketEndpoint.java | 2 +- 4 files changed, 313 insertions(+), 263 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index a98b0edf4..4d5c6f5d2 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -246,7 +246,7 @@ public class CommunicationConnection implements Runnable { * * @param message */ - public void notify(IoMessage 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 @@ -255,6 +255,10 @@ public class CommunicationConnection implements Runnable { 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()); @@ -328,7 +332,8 @@ public class CommunicationConnection implements Runnable { } /** - * Called when the message has been handled + * Called when an outgoing message has been handled. This method logs the message state and then notifies all + * observers * * @param message */ @@ -349,16 +354,9 @@ public class CommunicationConnection implements Runnable { default: logger.error(MessageFormat.format("Unhandled state for message {0}", message.toString())); //$NON-NLS-1$ break; + } - } - synchronized (this.connectionObservers) { - List observers = this.connectionObservers.getList(message.getKey()); - if (observers != null) { - for (ConnectionObserver observer : observers) { - observer.notify(message.getKey(), message); - } - } - } + notifyObservers(message); } public String getRemoteUri() { diff --git a/src/main/java/ch/eitchnet/communication/IoMessage.java b/src/main/java/ch/eitchnet/communication/IoMessage.java index 554a8d144..b8d95fb1b 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessage.java +++ b/src/main/java/ch/eitchnet/communication/IoMessage.java @@ -16,10 +16,25 @@ 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; @@ -28,7 +43,13 @@ public class IoMessage { 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; @@ -36,6 +57,7 @@ public class IoMessage { this.state = State.CREATED; this.stateMsg = StringHelper.DASH; this.updated = new Date(); + this.parameters = new HashMap<>(); } /** @@ -101,6 +123,36 @@ public class IoMessage { this.updated = new Date(); } + /** + * 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 + */ + public Object removeParam(String key) { + return this.parameters.remove(key); + } + + /** + * @return the set of parameter keys + */ + public Set getParamKeys() { + return this.parameters.keySet(); + } + @SuppressWarnings("nls") @Override public String toString() { diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java index edd9b0fd1..459bf36ab 100644 --- a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java @@ -1,251 +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.notify(message); - } - } - } -} +/* + * 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/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 5acc85e09..55d5edfe6 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -555,7 +555,7 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { // read and write from the connected server socket IoMessage message = this.messageVisitor.visit(this.inputStream, this.outputStream); if (message != null) { - this.connection.notify(message); + this.connection.handleNewMessage(message); } } From fe1216eb12b41d052cb69d53a582992afa831c51 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 15 Jan 2015 09:06:13 +0100 Subject: [PATCH 128/180] [Minor] Fixed returned ISO8601 duration for duration = 0 is now P0D --- src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java index 67c147e5c..61788c737 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -244,7 +244,7 @@ public class ISO8601Duration implements DurationFormat { throw new RuntimeException("A duration can not be negative!"); if (duration == 0l) - return "PT0S"; + return "P0D"; StringBuilder sb = new StringBuilder(); sb.append('P'); From f0421ba7db69c8a5d832d674d3db2753fb24281f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Jan 2015 13:57:53 +0100 Subject: [PATCH 129/180] [New] Added missing IoMessage.getParam() --- src/main/java/ch/eitchnet/communication/IoMessage.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/IoMessage.java b/src/main/java/ch/eitchnet/communication/IoMessage.java index b8d95fb1b..45d2ba77e 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessage.java +++ b/src/main/java/ch/eitchnet/communication/IoMessage.java @@ -123,6 +123,11 @@ public class IoMessage { this.updated = new Date(); } + @SuppressWarnings("unchecked") + public T getParam(String key) { + return (T) this.parameters.get(key); + } + /** * Add a transient parameter to this message * @@ -142,8 +147,9 @@ public class IoMessage { * The give of the parameter to be removed * @return the removed value, or null if the object didn't exist */ - public Object removeParam(String key) { - return this.parameters.remove(key); + @SuppressWarnings("unchecked") + public T removeParam(String key) { + return (T) this.parameters.remove(key); } /** From cde6eb652ec2c12ce22c8cb21a16589d56f8a49f Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Jan 2015 13:58:15 +0100 Subject: [PATCH 130/180] [Minor] add exception to log if observer updates fail --- .../java/ch/eitchnet/communication/CommunicationConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java index 4d5c6f5d2..46db7c6ba 100644 --- a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java +++ b/src/main/java/ch/eitchnet/communication/CommunicationConnection.java @@ -273,7 +273,7 @@ public class CommunicationConnection implements Runnable { 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())); + logger.error(MessageFormat.format(msg, message.getKey(), message.getId()), e); } } From 401052a5ea5844673615ba4c255b6faa96000adc Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 23 Jan 2015 10:23:38 +0100 Subject: [PATCH 131/180] [New] Added ConnectionMode.isSimulation() --- .../communication/ConnectionMode.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/src/main/java/ch/eitchnet/communication/ConnectionMode.java index fbc44a768..59d5d89d6 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMode.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMode.java @@ -38,18 +38,35 @@ 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, + OFF { + 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, + ON { + 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; + SIMULATION { + public boolean isSimulation() { + return true; + } + }; + + /** + * @return true if the current mode is simulation, false otherwise + */ + public abstract boolean isSimulation(); } From 97936b03fe86ce421bbfe61efbd51a49da1f0868 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 1 Feb 2015 13:14:49 +0100 Subject: [PATCH 132/180] [New] added es(), ei(), cs(), and ci() to StringMatchMode - these are short method names for the different match modes --- .../ch/eitchnet/utils/StringMatchMode.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/src/main/java/ch/eitchnet/utils/StringMatchMode.java index d096a0c53..aec70ad28 100644 --- a/src/main/java/ch/eitchnet/utils/StringMatchMode.java +++ b/src/main/java/ch/eitchnet/utils/StringMatchMode.java @@ -62,4 +62,64 @@ public enum StringMatchMode { return value1.equals(value2); } + + /** + *

    + * Returns {@link StringMatchMode#EQUALS_CASE_SENSITIVE} + *

    + * + *

    + * Short method, useful for static imports, or simply for shorter code + *

    + * + * @return {@link StringMatchMode#EQUALS_CASE_SENSITIVE} + */ + public static StringMatchMode es() { + return EQUALS_CASE_SENSITIVE; + } + + /** + *

    + * Returns {@link #EQUALS_CASE_INSENSITIVE} + *

    + * + *

    + * Short method, useful for static imports, or simply for shorter code + *

    + * + * @return {@link #EQUALS_CASE_INSENSITIVE} + */ + public static StringMatchMode ei() { + return EQUALS_CASE_INSENSITIVE; + } + + /** + *

    + * Returns {@link #CONTAINS_CASE_SENSITIVE} + *

    + * + *

    + * Short method, useful for static imports, or simply for shorter code + *

    + * + * @return {@link #CONTAINS_CASE_SENSITIVE} + */ + public static StringMatchMode cs() { + return CONTAINS_CASE_SENSITIVE; + } + + /** + *

    + * Returns {@link #CONTAINS_CASE_INSENSITIVE} + *

    + * + *

    + * Short method, useful for static imports, or simply for shorter code + *

    + * + * @return {@link #CONTAINS_CASE_INSENSITIVE} + */ + public static StringMatchMode ci() { + return CONTAINS_CASE_INSENSITIVE; + } } From d87517e4c2a3e9507b2661cc1737031761d99b61 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sat, 7 Feb 2015 19:29:38 +0100 Subject: [PATCH 133/180] [New] added Version.add(int, int, int) --- src/main/java/ch/eitchnet/utils/Version.java | 135 ++++++++++-------- .../java/ch/eitchnet/utils/VersionTest.java | 25 ++++ 2 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/Version.java b/src/main/java/ch/eitchnet/utils/Version.java index d8abaf340..e3507bbfb 100644 --- a/src/main/java/ch/eitchnet/utils/Version.java +++ b/src/main/java/ch/eitchnet/utils/Version.java @@ -40,22 +40,23 @@ import ch.eitchnet.utils.helper.StringHelper; *

    * *

    - * {@code Version} objects are immutable. + * 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 static final String SEPARATOR = "."; - private static final String OSGI_QUALIFIER_SEPARATOR = "."; - private static final String MAVEN_QUALIFIER_SEPARATOR = "-"; - - private static final String MAVEN_SNAPSHOT_QUALIFIER = "SNAPSHOT"; - private static final String OSGI_SNAPSHOT_QUALIFIER = "qualifier"; - private transient String versionString; private boolean osgiStyle; @@ -313,56 +314,36 @@ public class Version implements Comparable { 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; } /** - * 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. + * @return true if this version is for a snapshot version, i.e. ends with {@link #MAVEN_SNAPSHOT_QUALIFIER} or + * {@link #OSGI_SNAPSHOT_QUALIFIER} */ - @Override - public String toString() { - if (this.versionString != null) { - return this.versionString; - } - 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; - } else { - return MAVEN_SNAPSHOT_QUALIFIER; - } - } - return this.qualifier; + public boolean isSnapshot() { + return MAVEN_SNAPSHOT_QUALIFIER.equals(this.qualifier) || OSGI_SNAPSHOT_QUALIFIER.equals(this.qualifier); } /** @@ -460,6 +441,52 @@ public class Version implements Comparable { 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; + } else { + return MAVEN_SNAPSHOT_QUALIFIER; + } + } + return this.qualifier; + } + /** * @return This version represented in a maven compatible form. */ @@ -484,8 +511,4 @@ public class Version implements Comparable { result.append(this.minor); return result.toString(); } - - public boolean isSnapshot() { - return MAVEN_SNAPSHOT_QUALIFIER.equals(this.qualifier) || OSGI_SNAPSHOT_QUALIFIER.equals(this.qualifier); - } } diff --git a/src/test/java/ch/eitchnet/utils/VersionTest.java b/src/test/java/ch/eitchnet/utils/VersionTest.java index bd3c7304e..6e4708d80 100644 --- a/src/test/java/ch/eitchnet/utils/VersionTest.java +++ b/src/test/java/ch/eitchnet/utils/VersionTest.java @@ -75,4 +75,29 @@ public class VersionTest { assertEquals(Version.valueOf("7.6.1.qualifier").toOsgiStyleString(), "7.6.1.qualifier"); assertEquals(Version.valueOf("7.6.1.qualifier").toMavenStyleString(), "7.6.1-SNAPSHOT"); } + + @Test + public void shouldIncreaseVersion() { + + Version increased = Version.emptyVersion.add(0, 0, 0); + assertEquals("0.0.0", increased.toString()); + + increased = increased.add(0, 0, 1); + assertEquals("0.0.1", increased.toString()); + + increased = increased.add(0, 1, 0); + assertEquals("0.1.1", increased.toString()); + + increased = increased.add(1, 0, 0); + assertEquals("1.1.1", increased.toString()); + + increased = increased.add(-1, 0, 0); + assertEquals("0.1.1", increased.toString()); + + increased = increased.add(0, -1, 0); + assertEquals("0.0.1", increased.toString()); + + increased = increased.add(0, 0, -1); + assertEquals("0.0.0", increased.toString()); + } } From 8861db85091eea43f06ed2a480360ba670d3c30e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 8 Feb 2015 12:04:49 +0100 Subject: [PATCH 134/180] [New] added MapOfLists.isEmpty() and MapOfMaps.isEmpty() --- src/main/java/ch/eitchnet/utils/collections/MapOfLists.java | 4 ++++ src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java b/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java index dc64e2c79..e40c9a849 100644 --- a/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java @@ -117,4 +117,8 @@ public class MapOfLists { return 0; return list.size(); } + + public boolean isEmpty() { + return this.mapOfLists.isEmpty(); + } } diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java index 7e829ee0b..724caa53d 100644 --- a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java +++ b/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java @@ -165,4 +165,8 @@ public class MapOfMaps { return 0; return map.size(); } + + public boolean isEmpty() { + return this.mapOfMaps.isEmpty(); + } } From 72d470fd26350a7b1c79a0e8da8f29c1b9026e05 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 11 Feb 2015 23:01:37 +0100 Subject: [PATCH 135/180] [Minor] added more tests for Version --- .../java/ch/eitchnet/utils/VersionTest.java | 210 +++++++++--------- 1 file changed, 107 insertions(+), 103 deletions(-) diff --git a/src/test/java/ch/eitchnet/utils/VersionTest.java b/src/test/java/ch/eitchnet/utils/VersionTest.java index 6e4708d80..5381a6f8f 100644 --- a/src/test/java/ch/eitchnet/utils/VersionTest.java +++ b/src/test/java/ch/eitchnet/utils/VersionTest.java @@ -1,103 +1,107 @@ -package ch.eitchnet.utils; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -import ch.eitchnet.utils.helper.StringHelper; - -/** - * Tests the {@link Version} class - */ -public class VersionTest { - - @Test - public void shouldParseMajoMinoMicro() { - Version version = Version.valueOf("1.0.2"); - assertEquals(1, version.getMajor()); - assertEquals(0, version.getMinor()); - assertEquals(2, version.getMicro()); - assertEquals(StringHelper.EMPTY, version.getQualifier()); - assertEquals(StringHelper.EMPTY, version.getQualifier()); - - assertEquals("1.0.2", version.toString()); - } - - @Test - public void shouldParseVersion() { - { - Version version = Version.valueOf("7.5.6.1"); - assertEquals(7, version.getMajor()); - assertEquals(5, version.getMinor()); - assertEquals(6, version.getMicro()); - assertEquals("1", version.getQualifier()); - assertTrue(version.isOsgiStyle()); - } - { - Version version = Version.valueOf("7.5.6-1"); - assertEquals(7, version.getMajor()); - assertEquals(5, version.getMinor()); - assertEquals(6, version.getMicro()); - assertEquals("1", version.getQualifier()); - assertFalse(version.isOsgiStyle()); - } - } - - @Test - public void shouldCompareVersions() { - assertEquals(0, Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-1"))); - assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-2")) < 0); - assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.0")) < 0); - assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.1")) < 0); - assertTrue(Version.valueOf("7.5.6-alpha").compareTo(Version.valueOf("7.6.1-beta")) < 0); - assertTrue(Version.valueOf("7.7.0-0").compareTo(Version.valueOf("7.6.99-9")) > 0); - } - - @Test - public void shouldConvertToMajorMinorString() { - assertEquals("7.6", Version.valueOf("7.6.1-0").toMajorAndMinorString()); - } - - @Test - public void shouldKnowAboutBeingFullyQualified() { - assertFalse(Version.valueOf("7").isFullyQualified()); - assertFalse(Version.valueOf("7.6").isFullyQualified()); - assertFalse(Version.valueOf("7.6.1").isFullyQualified()); - assertTrue(Version.valueOf("7.6.1-0").isFullyQualified()); - } - - @Test - public void shouldDealWithEclipseStyleSnapshotQualifier() { - assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toOsgiStyleString(), "7.6.1.qualifier"); - assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toMavenStyleString(), "7.6.1-SNAPSHOT"); - assertEquals(Version.valueOf("7.6.1.qualifier").toOsgiStyleString(), "7.6.1.qualifier"); - assertEquals(Version.valueOf("7.6.1.qualifier").toMavenStyleString(), "7.6.1-SNAPSHOT"); - } - - @Test - public void shouldIncreaseVersion() { - - Version increased = Version.emptyVersion.add(0, 0, 0); - assertEquals("0.0.0", increased.toString()); - - increased = increased.add(0, 0, 1); - assertEquals("0.0.1", increased.toString()); - - increased = increased.add(0, 1, 0); - assertEquals("0.1.1", increased.toString()); - - increased = increased.add(1, 0, 0); - assertEquals("1.1.1", increased.toString()); - - increased = increased.add(-1, 0, 0); - assertEquals("0.1.1", increased.toString()); - - increased = increased.add(0, -1, 0); - assertEquals("0.0.1", increased.toString()); - - increased = increased.add(0, 0, -1); - assertEquals("0.0.0", increased.toString()); - } -} +package ch.eitchnet.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import ch.eitchnet.utils.helper.StringHelper; + +/** + * Tests the {@link Version} class + */ +public class VersionTest { + + @Test + public void shouldParseMajoMinoMicro() { + Version version = Version.valueOf("1.0.2"); + assertEquals(1, version.getMajor()); + assertEquals(0, version.getMinor()); + assertEquals(2, version.getMicro()); + assertEquals(StringHelper.EMPTY, version.getQualifier()); + assertEquals(StringHelper.EMPTY, version.getQualifier()); + + assertEquals("1.0.2", version.toString()); + } + + @Test + public void shouldParseVersion() { + { + Version version = Version.valueOf("7.5.6.1"); + assertEquals(7, version.getMajor()); + assertEquals(5, version.getMinor()); + assertEquals(6, version.getMicro()); + assertEquals("1", version.getQualifier()); + assertTrue(version.isOsgiStyle()); + } + { + Version version = Version.valueOf("7.5.6-1"); + assertEquals(7, version.getMajor()); + assertEquals(5, version.getMinor()); + assertEquals(6, version.getMicro()); + assertEquals("1", version.getQualifier()); + assertFalse(version.isOsgiStyle()); + } + } + + @Test + public void shouldCompareVersions() { + assertEquals(0, Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-1"))); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.5.6-2")) < 0); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.0")) < 0); + assertTrue(Version.valueOf("7.5.6-1").compareTo(Version.valueOf("7.6.1")) < 0); + assertTrue(Version.valueOf("7.5.6-alpha").compareTo(Version.valueOf("7.6.1-beta")) < 0); + assertTrue(Version.valueOf("7.7.0-0").compareTo(Version.valueOf("7.6.99-9")) > 0); + assertTrue(Version.valueOf("0.0.1.a").compareTo(Version.valueOf("0.0.1.b")) < 0); + assertTrue(Version.valueOf("0.0.1.b").compareTo(Version.valueOf("0.0.1.a")) > 0); + assertTrue(Version.valueOf("0.0.1.a").compareTo(Version.valueOf("0.0.1.c")) < 0); + assertTrue(Version.valueOf("0.0.1.a").compareTo(Version.valueOf("0.0.1.aa")) < 0); + } + + @Test + public void shouldConvertToMajorMinorString() { + assertEquals("7.6", Version.valueOf("7.6.1-0").toMajorAndMinorString()); + } + + @Test + public void shouldKnowAboutBeingFullyQualified() { + assertFalse(Version.valueOf("7").isFullyQualified()); + assertFalse(Version.valueOf("7.6").isFullyQualified()); + assertFalse(Version.valueOf("7.6.1").isFullyQualified()); + assertTrue(Version.valueOf("7.6.1-0").isFullyQualified()); + } + + @Test + public void shouldDealWithEclipseStyleSnapshotQualifier() { + assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toOsgiStyleString(), "7.6.1.qualifier"); + assertEquals(Version.valueOf("7.6.1-SNAPSHOT").toMavenStyleString(), "7.6.1-SNAPSHOT"); + assertEquals(Version.valueOf("7.6.1.qualifier").toOsgiStyleString(), "7.6.1.qualifier"); + assertEquals(Version.valueOf("7.6.1.qualifier").toMavenStyleString(), "7.6.1-SNAPSHOT"); + } + + @Test + public void shouldIncreaseVersion() { + + Version increased = Version.emptyVersion.add(0, 0, 0); + assertEquals("0.0.0", increased.toString()); + + increased = increased.add(0, 0, 1); + assertEquals("0.0.1", increased.toString()); + + increased = increased.add(0, 1, 0); + assertEquals("0.1.1", increased.toString()); + + increased = increased.add(1, 0, 0); + assertEquals("1.1.1", increased.toString()); + + increased = increased.add(-1, 0, 0); + assertEquals("0.1.1", increased.toString()); + + increased = increased.add(0, -1, 0); + assertEquals("0.0.1", increased.toString()); + + increased = increased.add(0, 0, -1); + assertEquals("0.0.0", increased.toString()); + } +} From e9277f975e5ef28b47f2001a4f9c035c947562b2 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 13 Feb 2015 11:45:34 +0100 Subject: [PATCH 136/180] [Minor] changed logging of ClientSocketEndpoint connection timeout --- .../eitchnet/communication/tcpip/ClientSocketEndpoint.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index 4d7945ad2..316eff4de 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -212,9 +212,8 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { this.closed = true; this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null); } catch (Exception e) { - String msg = "Error while connecting to {0}:{1}"; //$NON-NLS-1$ - logger.error(MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort))); - logger.error(e.getMessage(), 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()); } } From ccd43acf0a915e89620efa7094c9e9e1b4cf1a63 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 13 Feb 2015 11:46:47 +0100 Subject: [PATCH 137/180] [Minor] changed logging of ServerSocketEndpoint connection timeout --- .../eitchnet/communication/tcpip/ServerSocketEndpoint.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java index 55d5edfe6..57f6399ab 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java @@ -230,9 +230,8 @@ public class ServerSocketEndpoint implements CommunicationEndpoint, Runnable { logger.warn("Socket closed!"); //$NON-NLS-1$ this.connection.notifyStateChange(ConnectionState.DISCONNECTED, null); } else { - String msg = "Error while opening socket for inbound connection {0}"; //$NON-NLS-1$ - logger.error(MessageFormat.format(msg, this.connection.getId())); - logger.error(e.getMessage(), e); + 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()); } From 1279ed63f330519ec272b8ee489ed3843a8c7ea3 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 13 Feb 2015 12:55:12 +0100 Subject: [PATCH 138/180] [Minor] added some minor JavaDoc --- .../communication/IoMessageVisitor.java | 2 +- .../tcpip/SocketMessageVisitor.java | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java index a64207ec1..b5a08f9da 100644 --- a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java @@ -22,7 +22,7 @@ import ch.eitchnet.communication.console.ConsoleMessageVisitor; /** *

    - * Visitors to read and write {@link IoMessage} using different kind of endpoints. Different entpoints will require + * 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}. *

    diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java index 1c9610820..52f535300 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java @@ -17,10 +17,16 @@ 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; @@ -33,8 +39,24 @@ public abstract class SocketMessageVisitor extends IoMessageVisitor { 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; } From 109cfd4df4d5163dcc3e7fd2dbff9195d382fac1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 13 Feb 2015 15:30:00 +0100 Subject: [PATCH 139/180] [Bugfix] fixed situation where connection is not closed on exception - on timeouts the underlying socket doesn't know that the connection is ok, and thus we have exceptions, so we must make sure we close them properly --- .../eitchnet/communication/tcpip/ClientSocketEndpoint.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java index 316eff4de..7ea688be1 100644 --- a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java +++ b/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java @@ -213,7 +213,9 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { 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()); + logger.error( + MessageFormat.format(msg, this.remoteInputAddressS, Integer.toString(this.remoteInputPort)), + e.getMessage()); this.connection.notifyStateChange(ConnectionState.BROKEN, e.getLocalizedMessage()); } } @@ -521,12 +523,13 @@ public class ClientSocketEndpoint implements CommunicationEndpoint { 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) { + if (this.closeAfterSend && !this.closed) { closeConnection(); } } From 09966937c904113002d09e419c70b5945a761a4c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 2 Mar 2015 13:43:06 +0100 Subject: [PATCH 140/180] [Project] Bumped version to 1.1.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d784a611d..9839d0b9a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ ch.eitchnet ch.eitchnet.parent - 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT ../ch.eitchnet.parent/pom.xml From 07f009b7ff7cba427e4f0508da65f8d9b04db2f4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 8 Mar 2015 13:36:49 +0100 Subject: [PATCH 141/180] [New] Added XmlKeyValue for key value pairs in JAXB --- .../ch/eitchnet/utils/xml/XmlKeyValue.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java diff --git a/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java b/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java new file mode 100644 index 000000000..f813b230c --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java @@ -0,0 +1,73 @@ +package ch.eitchnet.utils.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.bind.annotation.XmlAttribute; + +public class XmlKeyValue { + + @XmlAttribute(name = "key") + private String key; + @XmlAttribute(name = "value") + private String value; + + public XmlKeyValue(String key, String value) { + this.key = key; + this.value = value; + } + + public XmlKeyValue() { + // no-arg constructor for JAXB + } + + /** + * @return the key + */ + public String getKey() { + return this.key; + } + + /** + * @param key + * the key to set + */ + public void setKey(String key) { + this.key = key; + } + + /** + * @return the value + */ + public String getValue() { + return this.value; + } + + /** + * @param value + * the value to set + */ + public void setValue(String value) { + this.value = value; + } + + public static List valueOf(Map map) { + List keyValues = new ArrayList<>(map.size()); + for (Entry entry : map.entrySet()) { + keyValues.add(new XmlKeyValue(entry.getKey(), entry.getValue())); + } + return keyValues; + } + + public static Map toMap(List values) { + Map propertyMap = new HashMap<>(values.size()); + for (XmlKeyValue xmlKeyValue : values) { + propertyMap.put(xmlKeyValue.getKey(), xmlKeyValue.getValue()); + } + + return propertyMap; + } +} \ No newline at end of file From 638cebe01e8275c188b9e444576569e6e29ae73e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 12 Mar 2015 13:18:20 +0100 Subject: [PATCH 142/180] [New] Added new Tuple to collections --- .../ch/eitchnet/utils/collections/Tuple.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/collections/Tuple.java diff --git a/src/main/java/ch/eitchnet/utils/collections/Tuple.java b/src/main/java/ch/eitchnet/utils/collections/Tuple.java new file mode 100644 index 000000000..ec61f3338 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/collections/Tuple.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.utils.collections; + +/** + * Simple wrapper for two elements + * + * @author Robert von Burg + */ +public class Tuple { + + private Object first; + private Object second; + + public Tuple() { + // + } + + public Tuple(T first, U second) { + this.first = first; + this.second = second; + } + + @SuppressWarnings("unchecked") + public T getFirst() { + return (T) this.first; + } + + public void setFirst(T first) { + this.first = first; + } + + @SuppressWarnings("unchecked") + public U getSecond() { + return (U) this.second; + } + + public void setSecond(U second) { + this.second = second; + } +} From 8e75a7651ae64b0cd47cfe923244d6828a9f4c93 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 15 Mar 2015 11:02:57 +0100 Subject: [PATCH 143/180] [Bugfix] fixed exception formatting bug in StringHelper --- src/main/java/ch/eitchnet/utils/helper/StringHelper.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index c6306e61c..069135ed6 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -589,8 +589,6 @@ public class StringHelper { public static String formatExceptionMessage(Throwable t) { StringBuilder sb = new StringBuilder(); sb.append(t.getMessage()); - sb.append("\n"); - appendCause(sb, t); return sb.toString(); } @@ -600,9 +598,10 @@ public class StringHelper { if (cause == null) return; + sb.append("\n"); + sb.append("cause:\n"); sb.append(cause.getMessage()); - sb.append("\n"); if (cause.getCause() != null) appendCause(sb, cause.getCause()); From 6338690ad2f849bdcb8c4c728b6bf96a87db42d8 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 22 Mar 2015 00:33:59 +0100 Subject: [PATCH 144/180] [New] Added StringHelper.getExceptionMessage(Throwable) --- .../ch/eitchnet/utils/helper/StringHelper.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 069135ed6..b0adc8e75 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -563,6 +563,23 @@ public class StringHelper { } } + /** + *

    + * 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 getExceptionMessage(Throwable t) { + return StringHelper.isEmpty(t.getMessage()) ? t.getClass().getName() : t.getMessage(); + } + /** * Formats the given {@link Throwable}'s stack trace to a string * From ca8f5b2f6a3e737de85914359085be3761bc67c3 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 16 Apr 2015 15:45:04 +0200 Subject: [PATCH 145/180] [New] added DateRange.toString() --- .../java/ch/eitchnet/utils/collections/DateRange.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/src/main/java/ch/eitchnet/utils/collections/DateRange.java index 3c25b4bc7..b4410b8a9 100644 --- a/src/main/java/ch/eitchnet/utils/collections/DateRange.java +++ b/src/main/java/ch/eitchnet/utils/collections/DateRange.java @@ -18,6 +18,7 @@ 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 @@ -122,4 +123,11 @@ public class DateRange { } 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)"); + } } From 556981777c129ea17cd4acc7d13e5e8162498ac6 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 19 Apr 2015 16:12:26 +0200 Subject: [PATCH 146/180] [New] Added IndentingXMLStreamWriter from javanet.staxutils --- .../java/javanet/staxutils/Indentation.java | 36 ++ .../staxutils/IndentingXMLStreamWriter.java | 370 ++++++++++++++++++ .../helpers/StreamWriterDelegate.java | 213 ++++++++++ 3 files changed, 619 insertions(+) create mode 100644 src/main/java/javanet/staxutils/Indentation.java create mode 100644 src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java create mode 100644 src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java diff --git a/src/main/java/javanet/staxutils/Indentation.java b/src/main/java/javanet/staxutils/Indentation.java new file mode 100644 index 000000000..844513789 --- /dev/null +++ b/src/main/java/javanet/staxutils/Indentation.java @@ -0,0 +1,36 @@ +package javanet.staxutils; + +/** + * Characters that represent line breaks and indentation. These are represented as String-valued JavaBean properties. + */ +@SuppressWarnings("nls") +public interface Indentation { + + /** Two spaces; the default indentation. */ + public static final String DEFAULT_INDENT = " "; + + /** + * Set the characters used for one level of indentation. The default is {@link #DEFAULT_INDENT}. "\t" is a popular + * alternative. + */ + void setIndent(String indent); + + /** The characters used for one level of indentation. */ + String getIndent(); + + /** + * "\n"; the normalized representation of end-of-line in XML. + */ + public static final String NORMAL_END_OF_LINE = "\n"; + + /** + * Set the characters that introduce a new line. The default is {@link #NORMAL_END_OF_LINE}. + * {@link IndentingXMLStreamWriter#getLineSeparator}() is a popular alternative. + */ + public void setNewLine(String newLine); + + /** The characters that introduce a new line. */ + String getNewLine(); + +} diff --git a/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java b/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java new file mode 100644 index 000000000..f4af62c62 --- /dev/null +++ b/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2006, John Kristian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of StAX-Utils nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package javanet.staxutils; + +import javanet.staxutils.helpers.StreamWriterDelegate; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * A filter that indents an XML stream. To apply it, construct a filter that contains another {@link XMLStreamWriter}, + * which you pass to the constructor. Then call methods of the filter instead of the contained stream. For example: + * + *
    + * {@link XMLStreamWriter} stream = ...
    + * stream = new {@link IndentingXMLStreamWriter}(stream);
    + * stream.writeStartDocument();
    + * ...
    + * 
    + * + *

    + * The filter inserts characters to format the document as an outline, with nested elements indented. Basically, it + * inserts a line break and whitespace before: + *

      + *
    • each DTD, processing instruction or comment that's not preceded by data
    • + *
    • each starting tag that's not preceded by data
    • + *
    • each ending tag that's preceded by nested elements but not data
    • + *
    + * This works well with 'data-oriented' XML, wherein each element contains either data or nested elements but not both. + * It can work badly with other styles of XML. For example, the data in a 'mixed content' document are apt to be + * polluted with indentation characters. + *

    + * Indentation can be adjusted by setting the newLine and indent properties. But set them to whitespace only, for best + * results. Non-whitespace is apt to cause problems, for example when this class attempts to insert newLine before the + * root element. + * + * @author John Kristian + */ +@SuppressWarnings("nls") +public class IndentingXMLStreamWriter extends StreamWriterDelegate implements Indentation { + + public IndentingXMLStreamWriter(XMLStreamWriter out) { + this(out, DEFAULT_INDENT, NORMAL_END_OF_LINE); + } + + public IndentingXMLStreamWriter(XMLStreamWriter out, String indent) { + this(out, indent, NORMAL_END_OF_LINE); + } + + public IndentingXMLStreamWriter(XMLStreamWriter out, String indent, String newLine) { + super(out); + setIndent(indent); + setNewLine(newLine); + } + + /** How deeply nested the current scope is. The root element is depth 1. */ + private int depth = 0; // document scope + + /** stack[depth] indicates what's been written into the current scope. */ + private int[] stack = new int[] { 0, 0, 0, 0 }; // nothing written yet + + private static final int WROTE_MARKUP = 1; + + private static final int WROTE_DATA = 2; + + private String indent = DEFAULT_INDENT; + + private String newLine = NORMAL_END_OF_LINE; + + /** newLine followed by copies of indent. */ + private char[] linePrefix = null; + + @Override + public void setIndent(String indent) { + if (!indent.equals(this.indent)) { + this.indent = indent; + this.linePrefix = null; + } + } + + @Override + public String getIndent() { + return this.indent; + } + + @Override + public void setNewLine(String newLine) { + if (!newLine.equals(this.newLine)) { + this.newLine = newLine; + this.linePrefix = null; + } + } + + /** + * @return System.getProperty("line.separator"); or {@link #NORMAL_END_OF_LINE} if that fails. + */ + public static String getLineSeparator() { + try { + return System.getProperty("line.separator"); + } catch (SecurityException ignored) { + // + } + return NORMAL_END_OF_LINE; + } + + @Override + public String getNewLine() { + return this.newLine; + } + + @Override + public void writeStartDocument() throws XMLStreamException { + beforeMarkup(); + this.out.writeStartDocument(); + afterMarkup(); + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException { + beforeMarkup(); + this.out.writeStartDocument(version); + afterMarkup(); + } + + @Override + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + beforeMarkup(); + this.out.writeStartDocument(encoding, version); + afterMarkup(); + } + + @Override + public void writeDTD(String dtd) throws XMLStreamException { + beforeMarkup(); + this.out.writeDTD(dtd); + afterMarkup(); + } + + @Override + public void writeProcessingInstruction(String target) throws XMLStreamException { + beforeMarkup(); + this.out.writeProcessingInstruction(target); + afterMarkup(); + } + + @Override + public void writeProcessingInstruction(String target, String data) throws XMLStreamException { + beforeMarkup(); + this.out.writeProcessingInstruction(target, data); + afterMarkup(); + } + + @Override + public void writeComment(String data) throws XMLStreamException { + beforeMarkup(); + this.out.writeComment(data); + afterMarkup(); + } + + @Override + public void writeEmptyElement(String localName) throws XMLStreamException { + beforeMarkup(); + this.out.writeEmptyElement(localName); + afterMarkup(); + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + beforeMarkup(); + this.out.writeEmptyElement(namespaceURI, localName); + afterMarkup(); + } + + @Override + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + beforeMarkup(); + this.out.writeEmptyElement(prefix, localName, namespaceURI); + afterMarkup(); + } + + @Override + public void writeStartElement(String localName) throws XMLStreamException { + beforeStartElement(); + this.out.writeStartElement(localName); + afterStartElement(); + } + + @Override + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + beforeStartElement(); + this.out.writeStartElement(namespaceURI, localName); + afterStartElement(); + } + + @Override + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + beforeStartElement(); + this.out.writeStartElement(prefix, localName, namespaceURI); + afterStartElement(); + } + + @Override + public void writeCharacters(String text) throws XMLStreamException { + this.out.writeCharacters(text); + afterData(); + } + + @Override + public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + this.out.writeCharacters(text, start, len); + afterData(); + } + + @Override + public void writeCData(String data) throws XMLStreamException { + this.out.writeCData(data); + afterData(); + } + + @Override + public void writeEntityRef(String name) throws XMLStreamException { + this.out.writeEntityRef(name); + afterData(); + } + + @Override + public void writeEndElement() throws XMLStreamException { + beforeEndElement(); + this.out.writeEndElement(); + afterEndElement(); + } + + @Override + public void writeEndDocument() throws XMLStreamException { + try { + while (this.depth > 0) { + writeEndElement(); // indented + } + } catch (Exception ignored) { + ignored.printStackTrace(); + } + this.out.writeEndDocument(); + afterEndDocument(); + } + + /** Prepare to write markup, by writing a new line and indentation. */ + protected void beforeMarkup() { + int soFar = this.stack[this.depth]; + if ((soFar & WROTE_DATA) == 0 // no data in this scope + && (this.depth > 0 || soFar != 0)) // not the first line + { + try { + writeNewLine(this.depth); + if (this.depth > 0 && getIndent().length() > 0) { + afterMarkup(); // indentation was written + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** Note that markup or indentation was written. */ + protected void afterMarkup() { + this.stack[this.depth] |= WROTE_MARKUP; + } + + /** Note that data were written. */ + protected void afterData() { + this.stack[this.depth] |= WROTE_DATA; + } + + /** Prepare to start an element, by allocating stack space. */ + protected void beforeStartElement() { + beforeMarkup(); + if (this.stack.length <= this.depth + 1) { + // Allocate more space for the stack: + int[] newStack = new int[this.stack.length * 2]; + System.arraycopy(this.stack, 0, newStack, 0, this.stack.length); + this.stack = newStack; + } + this.stack[this.depth + 1] = 0; // nothing written yet + } + + /** Note that an element was started. */ + protected void afterStartElement() { + afterMarkup(); + ++this.depth; + } + + /** Prepare to end an element, by writing a new line and indentation. */ + protected void beforeEndElement() { + if (this.depth > 0 && this.stack[this.depth] == WROTE_MARKUP) { // but not data + try { + writeNewLine(this.depth - 1); + } catch (Exception ignored) { + ignored.printStackTrace(); + } + } + } + + /** Note that an element was ended. */ + protected void afterEndElement() { + if (this.depth > 0) { + --this.depth; + } + } + + /** Note that a document was ended. */ + protected void afterEndDocument() { + if (this.stack[this.depth = 0] == WROTE_MARKUP) { // but not data + try { + writeNewLine(0); + } catch (Exception ignored) { + ignored.printStackTrace(); + } + } + this.stack[this.depth] = 0; // start fresh + } + + /** Write a line separator followed by indentation. */ + protected void writeNewLine(int indentation) throws XMLStreamException { + final int newLineLength = getNewLine().length(); + final int prefixLength = newLineLength + (getIndent().length() * indentation); + if (prefixLength > 0) { + if (this.linePrefix == null) { + this.linePrefix = (getNewLine() + getIndent()).toCharArray(); + } + while (prefixLength > this.linePrefix.length) { + // make linePrefix longer: + char[] newPrefix = new char[newLineLength + ((this.linePrefix.length - newLineLength) * 2)]; + System.arraycopy(this.linePrefix, 0, newPrefix, 0, this.linePrefix.length); + System.arraycopy(this.linePrefix, newLineLength, newPrefix, this.linePrefix.length, + this.linePrefix.length - newLineLength); + this.linePrefix = newPrefix; + } + this.out.writeCharacters(this.linePrefix, 0, prefixLength); + } + } + +} diff --git a/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java b/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java new file mode 100644 index 000000000..9922aafa7 --- /dev/null +++ b/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2006, John Kristian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of StAX-Utils nor the names of its contributors + * may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +package javanet.staxutils.helpers; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * Abstract class for writing filtered XML streams. This class provides methods that merely delegate to the contained + * stream. Subclasses should override some of these methods, and may also provide additional methods and fields. + * + * @author John Kristian + */ +public abstract class StreamWriterDelegate implements XMLStreamWriter { + + protected StreamWriterDelegate(XMLStreamWriter out) { + this.out = out; + } + + protected XMLStreamWriter out; + + @Override + public Object getProperty(String name) throws IllegalArgumentException { + return this.out.getProperty(name); + } + + @Override + public NamespaceContext getNamespaceContext() { + return this.out.getNamespaceContext(); + } + + @Override + public void setNamespaceContext(NamespaceContext context) throws XMLStreamException { + this.out.setNamespaceContext(context); + } + + @Override + public void setDefaultNamespace(String uri) throws XMLStreamException { + this.out.setDefaultNamespace(uri); + } + + @Override + public void writeStartDocument() throws XMLStreamException { + this.out.writeStartDocument(); + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException { + this.out.writeStartDocument(version); + } + + @Override + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + this.out.writeStartDocument(encoding, version); + } + + @Override + public void writeDTD(String dtd) throws XMLStreamException { + this.out.writeDTD(dtd); + } + + @Override + public void writeProcessingInstruction(String target) throws XMLStreamException { + this.out.writeProcessingInstruction(target); + } + + @Override + public void writeProcessingInstruction(String target, String data) throws XMLStreamException { + this.out.writeProcessingInstruction(target, data); + } + + @Override + public void writeComment(String data) throws XMLStreamException { + this.out.writeComment(data); + } + + @Override + public void writeEmptyElement(String localName) throws XMLStreamException { + this.out.writeEmptyElement(localName); + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + this.out.writeEmptyElement(namespaceURI, localName); + } + + @Override + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + this.out.writeEmptyElement(prefix, localName, namespaceURI); + } + + @Override + public void writeStartElement(String localName) throws XMLStreamException { + this.out.writeStartElement(localName); + } + + @Override + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + this.out.writeStartElement(namespaceURI, localName); + } + + @Override + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + this.out.writeStartElement(prefix, localName, namespaceURI); + } + + @Override + public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException { + this.out.writeDefaultNamespace(namespaceURI); + } + + @Override + public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException { + this.out.writeNamespace(prefix, namespaceURI); + } + + @Override + public String getPrefix(String uri) throws XMLStreamException { + return this.out.getPrefix(uri); + } + + @Override + public void setPrefix(String prefix, String uri) throws XMLStreamException { + this.out.setPrefix(prefix, uri); + } + + @Override + public void writeAttribute(String localName, String value) throws XMLStreamException { + this.out.writeAttribute(localName, value); + } + + @Override + public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException { + this.out.writeAttribute(namespaceURI, localName, value); + } + + @Override + public void writeAttribute(String prefix, String namespaceURI, String localName, String value) + throws XMLStreamException { + this.out.writeAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeCharacters(String text) throws XMLStreamException { + this.out.writeCharacters(text); + } + + @Override + public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + this.out.writeCharacters(text, start, len); + } + + @Override + public void writeCData(String data) throws XMLStreamException { + this.out.writeCData(data); + } + + @Override + public void writeEntityRef(String name) throws XMLStreamException { + this.out.writeEntityRef(name); + } + + @Override + public void writeEndElement() throws XMLStreamException { + this.out.writeEndElement(); + } + + @Override + public void writeEndDocument() throws XMLStreamException { + this.out.writeEndDocument(); + } + + @Override + public void flush() throws XMLStreamException { + this.out.flush(); + } + + @Override + public void close() throws XMLStreamException { + this.out.close(); + } + +} From dd1489772d243af5429201c5ac45a450d8635809 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 21 Apr 2015 15:03:30 +0200 Subject: [PATCH 147/180] [Major] Refactored DB package: Now use DataSource classes - this allows to inject a connection pool --- .../ch/eitchnet/db/DbConnectionCheck.java | 28 ++-- .../java/ch/eitchnet/db/DbConnectionInfo.java | 132 ------------------ .../ch/eitchnet/db/DbDataSourceBuilder.java | 28 ++++ .../java/ch/eitchnet/db/DbDriverLoader.java | 49 ------- .../ch/eitchnet/db/DbSchemaVersionCheck.java | 33 +++-- 5 files changed, 58 insertions(+), 212 deletions(-) delete mode 100644 src/main/java/ch/eitchnet/db/DbConnectionInfo.java create mode 100644 src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java delete mode 100644 src/main/java/ch/eitchnet/db/DbDriverLoader.java diff --git a/src/main/java/ch/eitchnet/db/DbConnectionCheck.java b/src/main/java/ch/eitchnet/db/DbConnectionCheck.java index 9177757ee..967cdacf3 100644 --- a/src/main/java/ch/eitchnet/db/DbConnectionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbConnectionCheck.java @@ -16,7 +16,6 @@ package ch.eitchnet.db; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -24,6 +23,8 @@ import java.text.MessageFormat; import java.util.Collection; import java.util.Map; +import javax.sql.DataSource; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,13 +34,13 @@ import org.slf4j.LoggerFactory; public class DbConnectionCheck { private static final Logger logger = LoggerFactory.getLogger(DbConnectionCheck.class); - private Map connetionInfoMap; + private Map dsMap; /** - * @param connetionInfoMap + * @param dsMap */ - public DbConnectionCheck(Map connetionInfoMap) { - this.connetionInfoMap = connetionInfoMap; + public DbConnectionCheck(Map dsMap) { + this.dsMap = dsMap; } /** @@ -48,17 +49,12 @@ public class DbConnectionCheck { * @throws DbException */ public void checkConnections() throws DbException { - Collection values = this.connetionInfoMap.values(); - for (DbConnectionInfo connectionInfo : values) { + Collection values = this.dsMap.values(); + for (DataSource ds : values) { - String url = connectionInfo.getUrl(); - String username = connectionInfo.getUsername(); - String password = connectionInfo.getPassword(); + logger.info("Checking connection " + ds); - logger.info("Checking connection " + username + "@" + url); - - try (Connection con = DriverManager.getConnection(url, username, password); - Statement st = con.createStatement();) { + try (Connection con = ds.getConnection(); Statement st = con.createStatement();) { try (ResultSet rs = st.executeQuery("select version()")) { //$NON-NLS-1$ if (rs.next()) { @@ -67,8 +63,8 @@ public class DbConnectionCheck { } } catch (SQLException e) { - String msg = "Failed to open DB connection to URL {0} due to: {1}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, url, e.getMessage()); + 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); } } diff --git a/src/main/java/ch/eitchnet/db/DbConnectionInfo.java b/src/main/java/ch/eitchnet/db/DbConnectionInfo.java deleted file mode 100644 index 391bb8d3f..000000000 --- a/src/main/java/ch/eitchnet/db/DbConnectionInfo.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.DriverManager; -import java.sql.SQLException; -import java.text.MessageFormat; - -import ch.eitchnet.utils.dbc.DBC; - -/** - * @author Robert von Burg - */ -public class DbConnectionInfo { - - private String realm; - private String url; - private String username; - private String password; - - public DbConnectionInfo(String realm, String url) { - DBC.PRE.assertNotEmpty("Realm must be set!", realm); //$NON-NLS-1$ - DBC.PRE.assertNotEmpty("Url must be set!", url); //$NON-NLS-1$ - this.realm = realm; - this.url = url; - } - - /** - * @return the realm - */ - public String getRealm() { - return this.realm; - } - - /** - * @param realm - * the realm to set - */ - public void setRealm(String realm) { - this.realm = realm; - } - - /** - * @return the url - */ - public String getUrl() { - return this.url; - } - - /** - * @param url - * the url to set - */ - public void setUrl(String url) { - this.url = url; - } - - /** - * @return the username - */ - public String getUsername() { - return this.username; - } - - /** - * @param username - * the username to set - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the password - */ - public String getPassword() { - return this.password; - } - - /** - * @param password - * the password to set - */ - public void setPassword(String password) { - this.password = password; - } - - @SuppressWarnings("nls") - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("DbConnectionInfo [realm="); - builder.append(this.realm); - builder.append(", url="); - builder.append(this.url); - builder.append(", username="); - builder.append(this.username); - builder.append(", password=***"); - builder.append("]"); - return builder.toString(); - } - - /** - * @return a {@link Connection} - * - * @throws DbException - */ - public Connection openConnection() throws DbException { - try { - Connection connection = DriverManager.getConnection(this.url, this.username, this.password); - connection.setAutoCommit(false); - return connection; - } catch (SQLException e) { - String msg = MessageFormat.format("Failed to get a connection for {0} due to {1}", this, e.getMessage()); //$NON-NLS-1$ - throw new DbException(msg, e); - } - } -} diff --git a/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java b/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java new file mode 100644 index 000000000..eb33af6f1 --- /dev/null +++ b/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/src/main/java/ch/eitchnet/db/DbDriverLoader.java b/src/main/java/ch/eitchnet/db/DbDriverLoader.java deleted file mode 100644 index a824cce1a..000000000 --- a/src/main/java/ch/eitchnet/db/DbDriverLoader.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.Driver; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.text.MessageFormat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Robert von Burg - */ -public class DbDriverLoader { - - private static final Logger logger = LoggerFactory.getLogger(DbDriverLoader.class); - - public static void loadDriverForConnection(DbConnectionInfo connectionInfo) throws DbException { - Driver driver; - try { - driver = DriverManager.getDriver(connectionInfo.getUrl()); - } catch (SQLException e) { - String msg = "Failed to load DB driver for URL {0} due to: {1}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, connectionInfo.getUrl(), e.getMessage()); - throw new DbException(msg, e); - } - - String compliant = driver.jdbcCompliant() ? "" : "non"; //$NON-NLS-1$ //$NON-NLS-2$ - String msg = "Realm {0}: Using {1} JDBC compliant Driver {2}.{3}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, connectionInfo.getRealm(), compliant, driver.getMajorVersion(), - driver.getMinorVersion()); - logger.info(msg); - } -} diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index d4c260219..f4c1efde4 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -21,7 +21,6 @@ import static ch.eitchnet.db.DbConstants.RESOURCE_DB_VERSION; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -29,8 +28,11 @@ 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; @@ -79,34 +81,34 @@ public class DbSchemaVersionCheck { return this.dbMigrationStates; } - public void checkSchemaVersion(Map connectionInfoMap) throws DbException { - for (DbConnectionInfo connectionInfo : connectionInfoMap.values()) { - DbMigrationState dbMigrationState = checkSchemaVersion(connectionInfo); - dbMigrationStates.put(connectionInfo.getRealm(), dbMigrationState); + 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); + 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(DbConnectionInfo connectionInfo) throws DbException { - String realm = connectionInfo.getRealm(); - String url = connectionInfo.getUrl(); - String username = connectionInfo.getUsername(); - String password = connectionInfo.getPassword(); + public DbMigrationState checkSchemaVersion(String realm, DataSource ds) throws DbException { - logger.info(MessageFormat.format("[{0}:{1}] Checking Schema version for: {2}@{3}", this.app, realm, username, - url)); + 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 = DriverManager.getConnection(url, username, password)) { + try (Connection con = ds.getConnection()) { // get current version Version currentVersion = getCurrentVersion(con, this.app); @@ -127,11 +129,12 @@ public class DbSchemaVersionCheck { break; } + con.commit(); return migrationType; } catch (SQLException e) { - String msg = "Failed to open DB connection to URL {0} due to: {1}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, url, e.getMessage()); + 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); } } From 1689ff69a93f7fcf491400e36b941c905d368018 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 8 May 2015 18:07:06 +0200 Subject: [PATCH 148/180] [New] added new DbConstants.PROP_DB_IGNORE_REALM --- src/main/java/ch/eitchnet/db/DbConstants.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ch/eitchnet/db/DbConstants.java b/src/main/java/ch/eitchnet/db/DbConstants.java index ceee7a85e..edec0cf3a 100644 --- a/src/main/java/ch/eitchnet/db/DbConstants.java +++ b/src/main/java/ch/eitchnet/db/DbConstants.java @@ -21,6 +21,7 @@ package ch.eitchnet.db; 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"; From 9e449e56eedc01ef24b9d4a8a5cbf5c52b013a48 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 3 Jul 2015 15:11:25 +0200 Subject: [PATCH 149/180] [Minor] Added method to write XML to string --- .../ch/eitchnet/utils/helper/XmlHelper.java | 136 +++++++++++++----- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index bf618b412..da2f03efc 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -15,10 +15,13 @@ */ package ch.eitchnet.utils.helper; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import javax.xml.parsers.DocumentBuilder; @@ -63,7 +66,7 @@ public class XmlHelper { /** * DEFAULT_ENCODING = "utf-8" : defines the default UTF-8 encoding expected of XML files */ - public static final String DEFAULT_ENCODING = "utf-8"; //$NON-NLS-1$ + public static final String DEFAULT_ENCODING = "UTF-8"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(XmlHelper.class); @@ -113,30 +116,118 @@ public class XmlHelper { } /** - * Writes a {@link Document} to an XML file on the file system + * Writes an {@link Element} to an XML file on the file system * - * @param document - * the {@link Document} to write to the file system + * @param rootElement + * the {@link Element} to write to the file system * @param file * the {@link File} describing the path on the file system where the XML file should be written to * * @throws RuntimeException * if something went wrong while creating the XML configuration, or writing the element */ - public static void writeDocument(Document document, File file) throws RuntimeException { + public static void writeElement(Element rootElement, File file) throws RuntimeException { + Document document = createDocument(); + document.appendChild(rootElement); + XmlHelper.writeDocument(document, file, DEFAULT_ENCODING); + } + /** + * Writes a {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param file + * the {@link File} describing the path on the file system where the XML file should be written to + * @param encoding + * encoding to use to write the file + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeDocument(Document document, File file) throws RuntimeException { + writeDocument(document, file, DEFAULT_ENCODING); + } + + /** + * Writes a {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param file + * the {@link File} describing the path on the file system where the XML file should be written to + * @param encoding + * encoding to use to write the file + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeDocument(Document document, File file, String encoding) throws RuntimeException { String msg = "Exporting document element {0} to {1}"; //$NON-NLS-1$ msg = MessageFormat.format(msg, document.getNodeName(), file.getAbsolutePath()); XmlHelper.logger.info(msg); + writeDocument(document, new StreamResult(file), encoding); + } + + /** + * Writes a {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param outputStream + * stream to write document to + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeDocument(Document document, OutputStream outputStream) throws RuntimeException { + writeDocument(document, new StreamResult(outputStream), DEFAULT_ENCODING); + } + + /** + * Writes a {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param outputStream + * stream to write document to + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static String writeToString(Document document) throws RuntimeException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeDocument(document, new StreamResult(out), DEFAULT_ENCODING); + return out.toString(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw new XmlException("Failed to create Document: " + e.getLocalizedMessage(), e); //$NON-NLS-1$ + } + } + + /** + * Writes a {@link Document} to an XML file on the file system + * + * @param document + * the {@link Document} to write to the file system + * @param file + * the {@link File} describing the path on the file system where the XML file should be written to + * @param encoding + * encoding to use to write the file + * + * @throws RuntimeException + * if something went wrong while creating the XML configuration, or writing the element + */ + public static void writeDocument(Document document, StreamResult streamResult, String encoding) + throws RuntimeException { String lineSep = System.getProperty(PROP_LINE_SEPARATOR); try { - String encoding = document.getInputEncoding(); - if (encoding == null || encoding.isEmpty()) { - XmlHelper.logger.info(MessageFormat.format( - "No encoding passed. Using default encoding {0}", XmlHelper.DEFAULT_ENCODING)); //$NON-NLS-1$ - encoding = XmlHelper.DEFAULT_ENCODING; + String docEncoding = document.getInputEncoding(); + if (docEncoding == null || docEncoding.isEmpty()) { + XmlHelper.logger.info(MessageFormat.format("No encoding passed. Using default encoding {0}", encoding)); //$NON-NLS-1$ + docEncoding = encoding; } if (!lineSep.equals("\n")) { //$NON-NLS-1$ @@ -150,14 +241,13 @@ public class XmlHelper { transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$ - transformer.setOutputProperty(OutputKeys.ENCODING, encoding); + transformer.setOutputProperty(OutputKeys.ENCODING, docEncoding); transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$ // transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", "\t"); // Transform to file - StreamResult result = new StreamResult(file); Source xmlSource = new DOMSource(document); - transformer.transform(xmlSource, result); + transformer.transform(xmlSource, streamResult); } catch (Exception e) { @@ -169,26 +259,6 @@ public class XmlHelper { } } - /** - * Writes an {@link Element} to an XML file on the file system - * - * @param rootElement - * the {@link Element} to write to the file system - * @param file - * the {@link File} describing the path on the file system where the XML file should be written to - * @param encoding - * encoding to use to write the file - * - * @throws RuntimeException - * if something went wrong while creating the XML configuration, or writing the element - */ - public static void writeElement(Element rootElement, File file, String encoding) throws RuntimeException { - - Document document = createDocument(); - document.appendChild(rootElement); - XmlHelper.writeDocument(document, file); - } - /** * Returns a new document instance * From 2e58db83fd35fea958431d7a2e9edae14ff6a20b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 7 Jul 2015 17:53:08 +0200 Subject: [PATCH 150/180] [Major] change DBC to add bad values for better debugging --- src/main/java/ch/eitchnet/utils/dbc/DBC.java | 33 ++++++++--------- .../java/ch/eitchnet/utils/dbc/DBCTest.java | 35 +++++++++++++------ 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/src/main/java/ch/eitchnet/utils/dbc/DBC.java index 68d1e1928..1b43adf76 100644 --- a/src/main/java/ch/eitchnet/utils/dbc/DBC.java +++ b/src/main/java/ch/eitchnet/utils/dbc/DBC.java @@ -17,6 +17,7 @@ package ch.eitchnet.utils.dbc; import java.io.File; import java.text.MessageFormat; +import java.util.Arrays; import java.util.Collection; import ch.eitchnet.utils.helper.StringHelper; @@ -38,8 +39,8 @@ public enum DBC { if (value2 != null && value2.equals(value1)) return; - String ex = "Values are not equal: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: {1} != {2}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, value1, value2); throw new DbcException(ex); } @@ -50,8 +51,8 @@ public enum DBC { if (value2 != null && !value2.equals(value1)) return; - String ex = "Values are equal: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: {1} == {2}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, value1, value2); throw new DbcException(ex); } @@ -73,8 +74,8 @@ public enum DBC { public void assertEmpty(String msg, String value) { if (!StringHelper.isEmpty(value)) { - String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: Illegal non-empty value: {1}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, value); throw new DbcException(ex); } } @@ -82,8 +83,8 @@ public enum DBC { public void assertEmpty(String msg, Object[] array) { assertNotNull(msg, array); if (array.length != 0) { - String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: Illegal non-empty value: {1}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, Arrays.toString(array)); throw new DbcException(ex); } } @@ -91,15 +92,15 @@ public enum DBC { public void assertEmpty(String msg, Collection collection) { assertNotNull(msg, collection); if (!collection.isEmpty()) { - String ex = "Illegal non-empty value: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: Illegal non-empty value: {1}"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, collection.toString()); throw new DbcException(ex); } } public void assertNotEmpty(String msg, String value) { if (StringHelper.isEmpty(value)) { - String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + String ex = "{0}: Illegal empty value"; //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -108,7 +109,7 @@ public enum DBC { public void assertNotEmpty(String msg, Object[] array) { assertNotNull(msg, array); if (array.length == 0) { - String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + String ex = "{0}: Illegal empty value"; //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -117,7 +118,7 @@ public enum DBC { public void assertNotEmpty(String msg, Collection collection) { assertNotNull(msg, collection); if (collection.isEmpty()) { - String ex = "Illegal empty value: {0}"; //$NON-NLS-1$ + String ex = "{0}: Illegal empty value"; //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -125,7 +126,7 @@ public enum DBC { public void assertNotNull(String msg, Object value) { if (value == null) { - String ex = "Illegal null value: {0}"; //$NON-NLS-1$ + String ex = "{0}: Illegal null value"; //$NON-NLS-1$ ex = MessageFormat.format(ex, msg); throw new DbcException(ex); } @@ -133,8 +134,8 @@ public enum DBC { public void assertNull(String msg, Object value) { if (value != null) { - String ex = "Illegal situation as value is not null: {0}"; //$NON-NLS-1$ - ex = MessageFormat.format(ex, msg); + String ex = "{0}: {1} != null"; //$NON-NLS-1$ + ex = MessageFormat.format(ex, msg, value); throw new DbcException(ex); } } diff --git a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java index 5877e46d2..7c18a3ab2 100644 --- a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java +++ b/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java @@ -16,6 +16,7 @@ package ch.eitchnet.utils.dbc; import java.io.File; +import java.text.MessageFormat; import org.junit.Rule; import org.junit.Test; @@ -64,10 +65,10 @@ public class DBCTest { @Test public void testAssertEquals_2() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Values are not equal:"); - String msg = ""; Object value1 = new Object(); Object value2 = new Object(); + String msg = MessageFormat.format("{0}: {1} != {2}", "", value1, value2); + this.exception.expectMessage(msg); DBC.PRE.assertEquals(msg, value1, value2); @@ -87,12 +88,13 @@ public class DBCTest { @Test public void testAssertEquals_3() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Values are not equal:"); - String msg = ""; Object value1 = null; Object value2 = new Object(); + String msg = MessageFormat.format("{0}: {1} != {2}", "", value1, value2); + this.exception.expectMessage(msg); + DBC.PRE.assertEquals(msg, value1, value2); // add additional test code here @@ -108,12 +110,13 @@ public class DBCTest { @Test public void testAssertEquals_4() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Values are not equal:"); - String msg = ""; Object value1 = new Object(); Object value2 = null; + String msg = MessageFormat.format("{0}: {1} != {2}", "", value1, value2); + this.exception.expectMessage(msg); + DBC.PRE.assertEquals(msg, value1, value2); // add additional test code here @@ -150,12 +153,15 @@ public class DBCTest { @Test public void testAssertNotEquals_1() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Values are equal:"); String msg = ""; Object value1 = null; Object value2 = null; + String ex = "{0}: {1} == {2}"; + ex = MessageFormat.format(ex, msg, value1, value2); + this.exception.expectMessage(ex); + DBC.PRE.assertNotEquals(msg, value1, value2); } @@ -217,12 +223,15 @@ public class DBCTest { @Test public void testAssertNotEquals_5() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Values are equal:"); String msg = ""; Object value1 = "bla"; Object value2 = "bla"; + String ex = "{0}: {1} == {2}"; + ex = MessageFormat.format(ex, msg, value1, value2); + this.exception.expectMessage(ex); + DBC.PRE.assertNotEquals(msg, value1, value2); } @@ -379,11 +388,14 @@ public class DBCTest { @Test public void testAssertNotNull_1() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Illegal null value:"); String msg = ""; Object value = null; + String ex = "{0}: Illegal null value"; + ex = MessageFormat.format(ex, msg, value); + this.exception.expectMessage(ex); + DBC.PRE.assertNotNull(msg, value); } @@ -414,11 +426,12 @@ public class DBCTest { @Test public void testAssertNull_1() throws Exception { this.exception.expect(DbcException.class); - this.exception.expectMessage("Illegal situation as value is not null:"); - String msg = ""; Object value = new Object(); + String msg = MessageFormat.format("{0}: {1} != null", "", value); + this.exception.expectMessage(msg); + DBC.PRE.assertNull(msg, value); } From ffbce7aab6caadb8e365065600e4e42cea5e62a0 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 9 Jul 2015 19:50:15 +0200 Subject: [PATCH 151/180] [New] Added a CsvParser --- .../java/ch/eitchnet/utils/csv/CsvParser.java | 154 ++++++++++++++++++ .../ch/eitchnet/utils/csv/CsvParserTest.java | 57 +++++++ src/test/resources/test_data.csv | 6 + 3 files changed, 217 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvParser.java create mode 100644 src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java create mode 100644 src/test/resources/test_data.csv diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java new file mode 100644 index 000000000..e94e09718 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java @@ -0,0 +1,154 @@ +/* + * 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.csv; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Scanner; + +import ch.eitchnet.utils.dbc.DBC; +import ch.eitchnet.utils.xml.XmlKeyValue; + +/** + * @author Robert von Burg + */ +public class CsvParser { + + private InputStream inputStream; + + /** + * @param inputStream + */ + public CsvParser(InputStream inputStream) { + DBC.PRE.assertNotNull("InputStream may not be null!", inputStream); + this.inputStream = inputStream; + } + + public CsvData parseData() { + + CsvData data = new CsvData(); + + int lineNr = 0; + boolean headerRead = false; + + try { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(this.inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + + line = line.trim(); + if (line.isEmpty()) + continue; + if (line.charAt(0) == '#') + continue; + + try (Scanner scanner = new Scanner(line)) { + scanner.useDelimiter(";"); + if (headerRead) { + int column = 0; + CsvRow row = new CsvRow(lineNr); + while (scanner.hasNext()) { + row.addColumnValue(data.getHeaderAtIndex(column), scanner.next()); + column++; + } + data.addRow(row); + } else { + while (scanner.hasNext()) { + data.addHeader(scanner.next().trim()); + } + headerRead = true; + } + } + + lineNr++; + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to read csv data at line " + lineNr + " due to " + e.getMessage(), e); + } + + return data; + } + + public class CsvData { + private List headers; + private List rows; + + public CsvData() { + this.headers = new ArrayList<>(); + this.rows = new ArrayList<>(); + } + + public String getHeaderAtIndex(int column) { + if (this.headers.size() < column + 1) { + throw new IllegalArgumentException("No header exists at column index " + column); + } + return this.headers.get(column); + } + + public void addHeader(String header) { + this.headers.add(header); + } + + public void addRow(CsvRow row) { + this.rows.add(row); + } + + public List getHeaders() { + return this.headers; + } + + public List getRows() { + return this.rows; + } + } + + public class CsvRow { + private int index; + private List values; + + public CsvRow(int index) { + this.index = index; + this.values = new ArrayList<>(); + } + + public int getIndex() { + return this.index; + } + + public void addColumnValue(String header, String value) { + this.values.add(new XmlKeyValue(header, value)); + } + + public String getColumnValue(String header) { + for (Iterator iter = this.values.iterator(); iter.hasNext();) { + XmlKeyValue next = iter.next(); + if (next.getKey().equals(header)) + return next.getValue(); + } + throw new IllegalArgumentException("No value exists for header" + header); + } + + public List getValues() { + return this.values; + } + } +} diff --git a/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java b/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java new file mode 100644 index 000000000..2a208c512 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java @@ -0,0 +1,57 @@ +/* + * 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.csv; + +import static org.junit.Assert.assertEquals; + +import java.io.InputStream; +import java.util.Arrays; + +import org.junit.Test; + +import ch.eitchnet.utils.csv.CsvParser.CsvData; +import ch.eitchnet.utils.csv.CsvParser.CsvRow; + +/** + * @author Robert von Burg + */ +public class CsvParserTest { + + @Test + public void shouldParseFile() { + + InputStream inputStream = CsvParserTest.class.getResourceAsStream("/test_data.csv"); + + CsvParser csvParser = new CsvParser(inputStream); + CsvData csvData = csvParser.parseData(); + + assertEquals(3, csvData.getHeaders().size()); + assertEquals(Arrays.asList("title", "description", "nrOfPages"), csvData.getHeaders()); + assertEquals(3, csvData.getRows().size()); + + CsvRow row = csvData.getRows().get(0); + assertEquals("A", row.getColumnValue("title")); + assertEquals("a", row.getColumnValue("description")); + assertEquals("1", row.getColumnValue("nrOfPages")); + + row = csvData.getRows().get(2); + assertEquals("C", row.getColumnValue("title")); + assertEquals("c", row.getColumnValue("description")); + assertEquals("3", row.getColumnValue("nrOfPages")); + + assertEquals(3, row.getValues().size()); + } +} diff --git a/src/test/resources/test_data.csv b/src/test/resources/test_data.csv new file mode 100644 index 000000000..d6b101a1e --- /dev/null +++ b/src/test/resources/test_data.csv @@ -0,0 +1,6 @@ +#Comment +title;description;nrOfPages +A;a;1 +# Another comment +B;b;2 +C;c;3 From 35bbb04d89cd6bd60ecf2ff1b49dba38a68f33a2 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 10 Jul 2015 10:47:44 +0200 Subject: [PATCH 152/180] [Minor] Changed exception handling in CsvParser --- src/main/java/ch/eitchnet/utils/csv/CsvParser.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java index e94e09718..7398b91b2 100644 --- a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java +++ b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java @@ -16,7 +16,6 @@ package ch.eitchnet.utils.csv; import java.io.BufferedReader; -import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; @@ -53,6 +52,7 @@ public class CsvParser { try (BufferedReader reader = new BufferedReader(new InputStreamReader(this.inputStream))) { String line; while ((line = reader.readLine()) != null) { + lineNr++; line = line.trim(); if (line.isEmpty()) @@ -64,7 +64,7 @@ public class CsvParser { scanner.useDelimiter(";"); if (headerRead) { int column = 0; - CsvRow row = new CsvRow(lineNr); + CsvRow row = new CsvRow(lineNr - 1); while (scanner.hasNext()) { row.addColumnValue(data.getHeaderAtIndex(column), scanner.next()); column++; @@ -77,11 +77,9 @@ public class CsvParser { headerRead = true; } } - - lineNr++; } } - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException("Failed to read csv data at line " + lineNr + " due to " + e.getMessage(), e); } From a3c2a2ed8f65c5463609f26c2835ce8f7baa5e69 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 10 Jul 2015 12:37:24 +0200 Subject: [PATCH 153/180] [Minor] moved CsvData and CsvRow to their own classes --- .../java/ch/eitchnet/utils/csv/CsvData.java | 52 ++++++++++++++ .../java/ch/eitchnet/utils/csv/CsvParser.java | 68 ------------------- .../java/ch/eitchnet/utils/csv/CsvRow.java | 53 +++++++++++++++ .../ch/eitchnet/utils/csv/CsvParserTest.java | 3 - 4 files changed, 105 insertions(+), 71 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvData.java create mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvRow.java diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvData.java b/src/main/java/ch/eitchnet/utils/csv/CsvData.java new file mode 100644 index 000000000..86b7053f0 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/csv/CsvData.java @@ -0,0 +1,52 @@ +/* + * 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.csv; + +import java.util.ArrayList; +import java.util.List; + +public class CsvData { + private List headers; + private List rows; + + public CsvData() { + this.headers = new ArrayList<>(); + this.rows = new ArrayList<>(); + } + + public String getHeaderAtIndex(int column) { + if (this.headers.size() < column + 1) { + throw new IllegalArgumentException("No header exists at column index " + column); + } + return this.headers.get(column); + } + + public void addHeader(String header) { + this.headers.add(header); + } + + public void addRow(CsvRow row) { + this.rows.add(row); + } + + public List getHeaders() { + return this.headers; + } + + public List getRows() { + return this.rows; + } +} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java index 7398b91b2..df088f6be 100644 --- a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java +++ b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java @@ -18,13 +18,9 @@ package ch.eitchnet.utils.csv; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; import java.util.Scanner; import ch.eitchnet.utils.dbc.DBC; -import ch.eitchnet.utils.xml.XmlKeyValue; /** * @author Robert von Burg @@ -85,68 +81,4 @@ public class CsvParser { return data; } - - public class CsvData { - private List headers; - private List rows; - - public CsvData() { - this.headers = new ArrayList<>(); - this.rows = new ArrayList<>(); - } - - public String getHeaderAtIndex(int column) { - if (this.headers.size() < column + 1) { - throw new IllegalArgumentException("No header exists at column index " + column); - } - return this.headers.get(column); - } - - public void addHeader(String header) { - this.headers.add(header); - } - - public void addRow(CsvRow row) { - this.rows.add(row); - } - - public List getHeaders() { - return this.headers; - } - - public List getRows() { - return this.rows; - } - } - - public class CsvRow { - private int index; - private List values; - - public CsvRow(int index) { - this.index = index; - this.values = new ArrayList<>(); - } - - public int getIndex() { - return this.index; - } - - public void addColumnValue(String header, String value) { - this.values.add(new XmlKeyValue(header, value)); - } - - public String getColumnValue(String header) { - for (Iterator iter = this.values.iterator(); iter.hasNext();) { - XmlKeyValue next = iter.next(); - if (next.getKey().equals(header)) - return next.getValue(); - } - throw new IllegalArgumentException("No value exists for header" + header); - } - - public List getValues() { - return this.values; - } - } } diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvRow.java b/src/main/java/ch/eitchnet/utils/csv/CsvRow.java new file mode 100644 index 000000000..e0939ac4f --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/csv/CsvRow.java @@ -0,0 +1,53 @@ +/* + * 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.csv; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import ch.eitchnet.utils.xml.XmlKeyValue; + +public class CsvRow { + private long index; + private List values; + + public CsvRow(long index) { + this.index = index; + this.values = new ArrayList<>(); + } + + public long getIndex() { + return this.index; + } + + public void addColumnValue(String header, String value) { + this.values.add(new XmlKeyValue(header, value)); + } + + public String getColumnValue(String header) { + for (Iterator iter = this.values.iterator(); iter.hasNext();) { + XmlKeyValue next = iter.next(); + if (next.getKey().equals(header)) + return next.getValue(); + } + throw new IllegalArgumentException("No value exists for header" + header); + } + + public List getValues() { + return this.values; + } +} \ No newline at end of file diff --git a/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java b/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java index 2a208c512..60d60efb6 100644 --- a/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java +++ b/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java @@ -22,9 +22,6 @@ import java.util.Arrays; import org.junit.Test; -import ch.eitchnet.utils.csv.CsvParser.CsvData; -import ch.eitchnet.utils.csv.CsvParser.CsvRow; - /** * @author Robert von Burg */ From 00a1fd4f72fc83b7f1603b1ddca8df376ed0f927 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Tue, 14 Jul 2015 09:15:40 +0200 Subject: [PATCH 154/180] [Major] removed CsvParser as the apache-commons-csv parser is better --- .../java/ch/eitchnet/utils/csv/CsvData.java | 52 ------------ .../java/ch/eitchnet/utils/csv/CsvParser.java | 84 ------------------- .../java/ch/eitchnet/utils/csv/CsvRow.java | 53 ------------ .../ch/eitchnet/utils/csv/CsvParserTest.java | 54 ------------ 4 files changed, 243 deletions(-) delete mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvData.java delete mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvParser.java delete mode 100644 src/main/java/ch/eitchnet/utils/csv/CsvRow.java delete mode 100644 src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvData.java b/src/main/java/ch/eitchnet/utils/csv/CsvData.java deleted file mode 100644 index 86b7053f0..000000000 --- a/src/main/java/ch/eitchnet/utils/csv/CsvData.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.csv; - -import java.util.ArrayList; -import java.util.List; - -public class CsvData { - private List headers; - private List rows; - - public CsvData() { - this.headers = new ArrayList<>(); - this.rows = new ArrayList<>(); - } - - public String getHeaderAtIndex(int column) { - if (this.headers.size() < column + 1) { - throw new IllegalArgumentException("No header exists at column index " + column); - } - return this.headers.get(column); - } - - public void addHeader(String header) { - this.headers.add(header); - } - - public void addRow(CsvRow row) { - this.rows.add(row); - } - - public List getHeaders() { - return this.headers; - } - - public List getRows() { - return this.rows; - } -} \ No newline at end of file diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java b/src/main/java/ch/eitchnet/utils/csv/CsvParser.java deleted file mode 100644 index df088f6be..000000000 --- a/src/main/java/ch/eitchnet/utils/csv/CsvParser.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.csv; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Scanner; - -import ch.eitchnet.utils.dbc.DBC; - -/** - * @author Robert von Burg - */ -public class CsvParser { - - private InputStream inputStream; - - /** - * @param inputStream - */ - public CsvParser(InputStream inputStream) { - DBC.PRE.assertNotNull("InputStream may not be null!", inputStream); - this.inputStream = inputStream; - } - - public CsvData parseData() { - - CsvData data = new CsvData(); - - int lineNr = 0; - boolean headerRead = false; - - try { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(this.inputStream))) { - String line; - while ((line = reader.readLine()) != null) { - lineNr++; - - line = line.trim(); - if (line.isEmpty()) - continue; - if (line.charAt(0) == '#') - continue; - - try (Scanner scanner = new Scanner(line)) { - scanner.useDelimiter(";"); - if (headerRead) { - int column = 0; - CsvRow row = new CsvRow(lineNr - 1); - while (scanner.hasNext()) { - row.addColumnValue(data.getHeaderAtIndex(column), scanner.next()); - column++; - } - data.addRow(row); - } else { - while (scanner.hasNext()) { - data.addHeader(scanner.next().trim()); - } - headerRead = true; - } - } - } - } - } catch (Exception e) { - throw new RuntimeException("Failed to read csv data at line " + lineNr + " due to " + e.getMessage(), e); - } - - return data; - } -} diff --git a/src/main/java/ch/eitchnet/utils/csv/CsvRow.java b/src/main/java/ch/eitchnet/utils/csv/CsvRow.java deleted file mode 100644 index e0939ac4f..000000000 --- a/src/main/java/ch/eitchnet/utils/csv/CsvRow.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.csv; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import ch.eitchnet.utils.xml.XmlKeyValue; - -public class CsvRow { - private long index; - private List values; - - public CsvRow(long index) { - this.index = index; - this.values = new ArrayList<>(); - } - - public long getIndex() { - return this.index; - } - - public void addColumnValue(String header, String value) { - this.values.add(new XmlKeyValue(header, value)); - } - - public String getColumnValue(String header) { - for (Iterator iter = this.values.iterator(); iter.hasNext();) { - XmlKeyValue next = iter.next(); - if (next.getKey().equals(header)) - return next.getValue(); - } - throw new IllegalArgumentException("No value exists for header" + header); - } - - public List getValues() { - return this.values; - } -} \ No newline at end of file diff --git a/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java b/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java deleted file mode 100644 index 60d60efb6..000000000 --- a/src/test/java/ch/eitchnet/utils/csv/CsvParserTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.csv; - -import static org.junit.Assert.assertEquals; - -import java.io.InputStream; -import java.util.Arrays; - -import org.junit.Test; - -/** - * @author Robert von Burg - */ -public class CsvParserTest { - - @Test - public void shouldParseFile() { - - InputStream inputStream = CsvParserTest.class.getResourceAsStream("/test_data.csv"); - - CsvParser csvParser = new CsvParser(inputStream); - CsvData csvData = csvParser.parseData(); - - assertEquals(3, csvData.getHeaders().size()); - assertEquals(Arrays.asList("title", "description", "nrOfPages"), csvData.getHeaders()); - assertEquals(3, csvData.getRows().size()); - - CsvRow row = csvData.getRows().get(0); - assertEquals("A", row.getColumnValue("title")); - assertEquals("a", row.getColumnValue("description")); - assertEquals("1", row.getColumnValue("nrOfPages")); - - row = csvData.getRows().get(2); - assertEquals("C", row.getColumnValue("title")); - assertEquals("c", row.getColumnValue("description")); - assertEquals("3", row.getColumnValue("nrOfPages")); - - assertEquals(3, row.getValues().size()); - } -} From 01c9da446cc46e2130131e04a830a0dac4e1b6c4 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 15 Jul 2015 07:52:25 +0200 Subject: [PATCH 155/180] [New] Added Paging Paging is used to page a list, i.e. return a sublist which has a certain size. --- .../ch/eitchnet/utils/collections/Paging.java | 111 ++++++++++++++++++ .../utils/collections/PagingTest.java | 78 ++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/collections/Paging.java create mode 100644 src/test/java/ch/eitchnet/utils/collections/PagingTest.java diff --git a/src/main/java/ch/eitchnet/utils/collections/Paging.java b/src/main/java/ch/eitchnet/utils/collections/Paging.java new file mode 100644 index 000000000..a7e906677 --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/collections/Paging.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.collections; + +import java.util.List; + +/** + * @author Robert von Burg + */ +public class Paging { + + private int resultsPerPage; + private int indexOfPageToReturn; + private int nrOfPages; + + private List input; + private List page; + + private Paging(int resultsPerPage, int indexOfPageToReturn) { + this.resultsPerPage = resultsPerPage; + this.indexOfPageToReturn = indexOfPageToReturn; + } + + public int getResultsPerPage() { + return this.resultsPerPage; + } + + public void setResultsPerPage(int resultsPerPage) { + this.resultsPerPage = resultsPerPage; + } + + public int getIndexOfPageToReturn() { + return this.indexOfPageToReturn; + } + + public void setIndexOfPageToReturn(int indexOfPageToReturn) { + this.indexOfPageToReturn = indexOfPageToReturn; + } + + public int getNrOfPages() { + return this.nrOfPages; + } + + public void setNrOfPages(int nrOfPages) { + this.nrOfPages = nrOfPages; + } + + public List getInput() { + return this.input; + } + + public List getPage() { + return this.page; + } + + /** + * Creates a sub list of the given list by creating defining start and end from the requested page of the form + * + * @param list + * the list to paginate + * @param resultsPerPage + * The number of items to return in each page + * @param page + * the page to return - start index is 1 + * + * @return a {@link Paging} instance from which the selected page (list) can be retrieved + */ + public static Paging asPage(List list, int resultsPerPage, int page) { + + Paging paging = new Paging(resultsPerPage, page); + + if (paging.resultsPerPage < 0 || paging.indexOfPageToReturn < 0) { + paging.nrOfPages = -1; + paging.input = list; + paging.page = list; + return paging; + } + + int size = list.size(); + + // calculate maximum number of pages + paging.nrOfPages = size / paging.resultsPerPage; + if (size % paging.resultsPerPage != 0) + paging.nrOfPages++; + + // and from this validate requested page + paging.indexOfPageToReturn = Math.min(paging.indexOfPageToReturn, paging.nrOfPages); + + // now we can calculate the start and end of the page + int start = Math.max(0, paging.resultsPerPage * paging.indexOfPageToReturn - paging.resultsPerPage); + int end = Math.min(size, paging.resultsPerPage * paging.indexOfPageToReturn); + + // and return the list + paging.page = list.subList(start, end); + + return paging; + } +} diff --git a/src/test/java/ch/eitchnet/utils/collections/PagingTest.java b/src/test/java/ch/eitchnet/utils/collections/PagingTest.java new file mode 100644 index 000000000..12e4a7036 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/collections/PagingTest.java @@ -0,0 +1,78 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +/** + * @author Robert von Burg + */ +public class PagingTest { + + @Test + public void shouldReturnAll() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, -1, -1).getPage(); + assertEquals(list, page); + } + + @Test + public void shouldReturnFirstPage1() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 1, 1).getPage(); + assertEquals(Arrays.asList("a"), page); + } + + @Test + public void shouldReturnFirstPage2() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 2, 1).getPage(); + assertEquals(Arrays.asList("a", "b"), page); + } + + @Test + public void shouldReturnSecondPage1() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 1, 2).getPage(); + assertEquals(Arrays.asList("b"), page); + } + + @Test + public void shouldReturnSecondPage2() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 2, 2).getPage(); + assertEquals(Arrays.asList("c", "d"), page); + } + + @Test + public void shouldReturnLastPage1() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 1, 6).getPage(); + assertEquals(Arrays.asList("f"), page); + } + + @Test + public void shouldReturnLastPage2() { + List list = Arrays.asList("a", "b", "c", "d", "e", "f"); + List page = Paging.asPage(list, 2, 3).getPage(); + assertEquals(Arrays.asList("e", "f"), page); + } +} From 2b9d09632ccd2802a89541a03d0eb5f9c9b81b40 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 15 Jul 2015 10:49:47 +0200 Subject: [PATCH 156/180] [Major] refactorings of Paging --- .../ch/eitchnet/utils/collections/Paging.java | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/collections/Paging.java b/src/main/java/ch/eitchnet/utils/collections/Paging.java index a7e906677..a649217a7 100644 --- a/src/main/java/ch/eitchnet/utils/collections/Paging.java +++ b/src/main/java/ch/eitchnet/utils/collections/Paging.java @@ -22,32 +22,33 @@ import java.util.List; */ public class Paging { - private int resultsPerPage; - private int indexOfPageToReturn; + private int pageSize; + private int pageToReturn; private int nrOfPages; + private int nrOfElements; private List input; private List page; - private Paging(int resultsPerPage, int indexOfPageToReturn) { - this.resultsPerPage = resultsPerPage; - this.indexOfPageToReturn = indexOfPageToReturn; + private Paging(int pageSize, int indexOfPageToReturn) { + this.pageSize = pageSize; + this.pageToReturn = indexOfPageToReturn; } - public int getResultsPerPage() { - return this.resultsPerPage; + public int getPageSize() { + return this.pageSize; } - public void setResultsPerPage(int resultsPerPage) { - this.resultsPerPage = resultsPerPage; + public void setPageSize(int pageSize) { + this.pageSize = pageSize; } - public int getIndexOfPageToReturn() { - return this.indexOfPageToReturn; + public int getPageToReturn() { + return this.pageToReturn; } - public void setIndexOfPageToReturn(int indexOfPageToReturn) { - this.indexOfPageToReturn = indexOfPageToReturn; + public void setPageToReturn(int pageToReturn) { + this.pageToReturn = pageToReturn; } public int getNrOfPages() { @@ -58,6 +59,14 @@ public class Paging { this.nrOfPages = nrOfPages; } + public int getNrOfElements() { + return this.nrOfElements; + } + + public void setNrOfElements(int nrOfElements) { + this.nrOfElements = nrOfElements; + } + public List getInput() { return this.input; } @@ -71,19 +80,22 @@ public class Paging { * * @param list * the list to paginate - * @param resultsPerPage + * @param pageSize * The number of items to return in each page * @param page * the page to return - start index is 1 * * @return a {@link Paging} instance from which the selected page (list) can be retrieved */ - public static Paging asPage(List list, int resultsPerPage, int page) { + public static Paging asPage(List list, int pageSize, int page) { - Paging paging = new Paging(resultsPerPage, page); + Paging paging = new Paging(pageSize, page); + paging.nrOfElements = list.size(); - if (paging.resultsPerPage < 0 || paging.indexOfPageToReturn < 0) { - paging.nrOfPages = -1; + if (paging.pageSize <= 0 || paging.pageToReturn <= 0) { + paging.nrOfPages = 1; + paging.pageSize = list.size(); + paging.pageToReturn = 1; paging.input = list; paging.page = list; return paging; @@ -92,16 +104,16 @@ public class Paging { int size = list.size(); // calculate maximum number of pages - paging.nrOfPages = size / paging.resultsPerPage; - if (size % paging.resultsPerPage != 0) + paging.nrOfPages = size / paging.pageSize; + if (size % paging.pageSize != 0) paging.nrOfPages++; // and from this validate requested page - paging.indexOfPageToReturn = Math.min(paging.indexOfPageToReturn, paging.nrOfPages); + paging.pageToReturn = Math.min(paging.pageToReturn, paging.nrOfPages); // now we can calculate the start and end of the page - int start = Math.max(0, paging.resultsPerPage * paging.indexOfPageToReturn - paging.resultsPerPage); - int end = Math.min(size, paging.resultsPerPage * paging.indexOfPageToReturn); + int start = Math.max(0, paging.pageSize * paging.pageToReturn - paging.pageSize); + int end = Math.min(size, paging.pageSize * paging.pageToReturn); // and return the list paging.page = list.subList(start, end); From 2d922df5724ca10d521e768df65d389677b28af6 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 15 Jul 2015 10:50:12 +0200 Subject: [PATCH 157/180] [New] Added new ExceptionHelper - moved exception helper methods from StringHelper to ExceptionHelper --- .../utils/helper/ExceptionHelper.java | 87 +++++++++++++++++++ .../eitchnet/utils/helper/StringHelper.java | 54 ++---------- 2 files changed, 93 insertions(+), 48 deletions(-) create mode 100644 src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java new file mode 100644 index 000000000..cfe5166dd --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java @@ -0,0 +1,87 @@ +/* + * 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 + *

    + * + * @param t + * @return + */ + public static String getExceptionMessage(Throwable t) { + return StringHelper.isEmpty(t.getMessage()) ? t.getClass().getName() : t.getMessage(); + } + + /** + * 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) { + StringBuilder sb = new StringBuilder(); + sb.append(t.getMessage()); + appendCause(sb, t); + return sb.toString(); + } + + private static void appendCause(StringBuilder sb, Throwable e) { + Throwable cause = e.getCause(); + if (cause == null) + return; + + sb.append("\n"); + + sb.append("cause:\n"); + sb.append(cause.getMessage()); + + if (cause.getCause() != null) + appendCause(sb, cause.getCause()); + } + +} diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index b0adc8e75..ecd20775c 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -15,8 +15,6 @@ */ package ch.eitchnet.utils.helper; -import java.io.PrintWriter; -import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -564,64 +562,24 @@ public class StringHelper { } /** - *

    - * 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 + * @see ExceptionHelper#formatException(Throwable) */ public static String getExceptionMessage(Throwable t) { - return StringHelper.isEmpty(t.getMessage()) ? t.getClass().getName() : t.getMessage(); + return ExceptionHelper.getExceptionMessage(t); } /** - * 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 + * @see ExceptionHelper#formatException(Throwable) */ public static String formatException(Throwable t) { - StringWriter stringWriter = new StringWriter(); - PrintWriter writer = new PrintWriter(stringWriter); - t.printStackTrace(writer); - return stringWriter.toString(); + return ExceptionHelper.formatException(t); } /** - * 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 + * @see ExceptionHelper#formatExceptionMessage(Throwable) */ public static String formatExceptionMessage(Throwable t) { - StringBuilder sb = new StringBuilder(); - sb.append(t.getMessage()); - appendCause(sb, t); - return sb.toString(); - } - - private static void appendCause(StringBuilder sb, Throwable e) { - Throwable cause = e.getCause(); - if (cause == null) - return; - - sb.append("\n"); - - sb.append("cause:\n"); - sb.append(cause.getMessage()); - - if (cause.getCause() != null) - appendCause(sb, cause.getCause()); + return ExceptionHelper.formatExceptionMessage(t); } /** From f5cf3e3ad0a4865ec75ffb4e58b03557b4d1db2e Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 10 Aug 2015 22:20:40 +0200 Subject: [PATCH 158/180] [New] Added a DefaultedHashMap for default values on unmapped keys --- .../utils/collections/DefaultedHashMap.java | 43 ++++++++++++++++ .../collections/DefaultedHashMapTest.java | 51 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java create mode 100644 src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java diff --git a/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java b/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java new file mode 100644 index 000000000..10ef2d75b --- /dev/null +++ b/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/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java b/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java new file mode 100644 index 000000000..ddf7c6569 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java @@ -0,0 +1,51 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +/** + * @author Robert von Burg + */ +public class DefaultedHashMapTest { + + private Map map; + + @Before + public void setUp() { + this.map = new DefaultedHashMap("foobar"); + this.map.put("foo", "foofoo"); + } + + @Test + public void shouldReturnMappedValue() { + assertTrue(this.map.containsKey("foo")); + assertEquals("foofoo", this.map.get("foo")); + } + + @Test + public void shouldReturnDefaultValue() { + assertFalse(this.map.containsKey("bar")); + assertEquals("foobar", this.map.get("bar")); + } +} From 4b9e166025c85df5c427ed1e15bc3a1d98ff9252 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 12 Aug 2015 20:40:57 +0200 Subject: [PATCH 159/180] [Minor] fixed Paging so it fits expected result for jQuery DataTable --- src/main/java/ch/eitchnet/utils/collections/Paging.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/collections/Paging.java b/src/main/java/ch/eitchnet/utils/collections/Paging.java index a649217a7..7031c34e4 100644 --- a/src/main/java/ch/eitchnet/utils/collections/Paging.java +++ b/src/main/java/ch/eitchnet/utils/collections/Paging.java @@ -93,9 +93,9 @@ public class Paging { paging.nrOfElements = list.size(); if (paging.pageSize <= 0 || paging.pageToReturn <= 0) { - paging.nrOfPages = 1; + paging.nrOfPages = 0; paging.pageSize = list.size(); - paging.pageToReturn = 1; + paging.pageToReturn = 0; paging.input = list; paging.page = list; return paging; @@ -118,6 +118,10 @@ public class Paging { // and return the list paging.page = list.subList(start, end); + // fix page size + if (paging.page.size() < paging.pageSize) + paging.pageSize = paging.page.size(); + return paging; } } From 363c21d30a9631d94adf5a4b508f2ddb8494ec27 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Sun, 23 Aug 2015 16:28:24 +0200 Subject: [PATCH 160/180] [Minor] code cleanup --- src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index f4c1efde4..d621a4c58 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -86,7 +86,7 @@ public class DbSchemaVersionCheck { String realm = entry.getKey(); DataSource ds = entry.getValue(); DbMigrationState dbMigrationState = checkSchemaVersion(realm, ds); - dbMigrationStates.put(realm, dbMigrationState); + this.dbMigrationStates.put(realm, dbMigrationState); } } @@ -248,7 +248,8 @@ public class DbSchemaVersionCheck { 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); + 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, From 67d1052fd3d6e38a8761596b6a27d509fd4318e1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 1 Oct 2015 07:59:07 +0200 Subject: [PATCH 161/180] [New] Added StringHelper.valueOrDash --- .../java/ch/eitchnet/utils/helper/StringHelper.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index ecd20775c..8f82b3487 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -657,6 +657,19 @@ public class StringHelper { 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 * From f59f4c5c0fd4201e9e5a81ed26f853e99226725c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 2 Oct 2015 08:19:07 +0200 Subject: [PATCH 162/180] [New] Added ExceptionHelper.getExceptionMessageWithCauses() and tests --- .../utils/helper/ExceptionHelper.java | 49 ++++++++++++------- .../utils/helper/ExceptionHelperTest.java | 46 +++++++++++++++++ 2 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java index cfe5166dd..916489e71 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java @@ -40,6 +40,29 @@ public class ExceptionHelper { return StringHelper.isEmpty(t.getMessage()) ? t.getClass().getName() : t.getMessage(); } + /** + *

    + * 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 * @@ -64,24 +87,12 @@ public class ExceptionHelper { * @return a string representation of the given {@link Throwable}'s messages including causes */ public static String formatExceptionMessage(Throwable t) { - StringBuilder sb = new StringBuilder(); - sb.append(t.getMessage()); - appendCause(sb, t); - return sb.toString(); + + if (t.getCause() == null) { + return getExceptionMessage(t); + } + + String root = formatExceptionMessage(t.getCause()); + return getExceptionMessage(t) + "\ncause:\n" + root; } - - private static void appendCause(StringBuilder sb, Throwable e) { - Throwable cause = e.getCause(); - if (cause == null) - return; - - sb.append("\n"); - - sb.append("cause:\n"); - sb.append(cause.getMessage()); - - if (cause.getCause() != null) - appendCause(sb, cause.getCause()); - } - } diff --git a/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java new file mode 100644 index 000000000..eba21340e --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java @@ -0,0 +1,46 @@ +package ch.eitchnet.utils.helper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class ExceptionHelperTest { + + @Test + public void shouldGetExceptionMsg() { + + Exception e = nestedException(); + assertEquals("Third", ExceptionHelper.getExceptionMessage(e)); + assertEquals("Third\nSecond\nFirst", ExceptionHelper.getExceptionMessageWithCauses(e)); + } + + @Test + public void shouldFormatException() { + + Exception e = nestedException(); + String formatException = ExceptionHelper.formatException(e); + assertTrue(formatException.contains("java.lang.RuntimeException: First")); + assertTrue(formatException.contains("java.lang.RuntimeException: Second")); + assertTrue(formatException.contains("java.lang.RuntimeException: Third")); + + formatException = ExceptionHelper.formatExceptionMessage(e); + assertEquals("Third\ncause:\nSecond\ncause:\nFirst", formatException); + } + + private Exception nestedException() { + try { + try { + try { + throw new RuntimeException("First"); + } catch (Exception e) { + throw new RuntimeException("Second", e); + } + } catch (Exception e) { + throw new RuntimeException("Third", e); + } + } catch (Exception e) { + return e; + } + } +} From 13b0003494611567b2f48ea5e7081f9ed3d568c3 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 15 Oct 2015 07:57:04 +0200 Subject: [PATCH 163/180] [New] Added AESCryptoHelper with tests --- .../utils/helper/AesCryptoHelper.java | 351 ++++++++++++++ .../utils/helper/AesCryptoHelperTest.java | 127 +++++ src/test/resources/crypto_test_image.ico | Bin 0 -> 5430 bytes src/test/resources/crypto_test_long.txt | 447 ++++++++++++++++++ src/test/resources/crypto_test_middle.txt | 3 + src/test/resources/crypto_test_short.txt | 1 + 6 files changed, 929 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java create mode 100644 src/test/resources/crypto_test_image.ico create mode 100644 src/test/resources/crypto_test_long.txt create mode 100644 src/test/resources/crypto_test_middle.txt create mode 100644 src/test/resources/crypto_test_short.txt diff --git a/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java b/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java new file mode 100644 index 000000000..1f580405c --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java @@ -0,0 +1,351 @@ +package ch.eitchnet.utils.helper; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.eitchnet.utils.dbc.DBC; + +public class AesCryptoHelper { + + private static final String CIPHER = "AES/CBC/PKCS5Padding"; + + private static final Logger logger = LoggerFactory.getLogger(AesCryptoHelper.class); + + public static OutputStream wrapEncrypt(char[] password, byte[] salt, OutputStream outputStream) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + return wrapEncrypt(secret, outputStream); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static OutputStream wrapEncrypt(SecretKey secret, OutputStream outputStream) { + + try { + + // set up cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, secret); + + // set up the initialization vector + AlgorithmParameters params = cipher.getParameters(); + byte[] initVector = params.getParameterSpec(IvParameterSpec.class).getIV(); + DBC.INTERIM.assertEquals("IV must be 16 bytes long!", 16, initVector.length); + + // write the initialization vector, but not through the cipher output stream! + outputStream.write(initVector); + outputStream.flush(); + + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); + return cipherOutputStream; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static InputStream wrapDecrypt(char[] password, byte[] salt, InputStream inputStream) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + return wrapDecrypt(secret, inputStream); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static InputStream wrapDecrypt(SecretKey secret, InputStream inputStream) { + + try { + + // read the initialization vector from the normal input stream + byte[] initVector = new byte[16]; + inputStream.read(initVector); + + // init cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(initVector)); + + CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); + return cipherInputStream; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void encrypt(char[] password, byte[] salt, String clearTextFileS, String encryptedFileS) { + + try (FileInputStream inFile = new FileInputStream(clearTextFileS); + FileOutputStream outFile = new FileOutputStream(encryptedFileS)) { + + encrypt(password, salt, inFile, outFile); + + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt file " + clearTextFileS + " to " + encryptedFileS, e); + } + + logger.info("Encrypted file " + clearTextFileS + " to " + encryptedFileS); + } + + public static void encrypt(SecretKey secret, String clearTextFileS, String encryptedFileS) { + + try (FileInputStream inFile = new FileInputStream(clearTextFileS); + FileOutputStream outFile = new FileOutputStream(encryptedFileS)) { + + encrypt(secret, inFile, outFile); + + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt file " + clearTextFileS + " to " + encryptedFileS, e); + } + + logger.info("Encrypted file " + clearTextFileS + " to " + encryptedFileS); + } + + public static void encrypt(char[] password, byte[] salt, InputStream inFile, OutputStream outFile) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + encrypt(secret, inFile, outFile); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void encrypt(SecretKey secret, InputStream inFile, OutputStream outFile) { + + try { + + // set up cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, secret); + + // set up the initialization vector + AlgorithmParameters params = cipher.getParameters(); + byte[] initVector = params.getParameterSpec(IvParameterSpec.class).getIV(); + DBC.INTERIM.assertEquals("IV must be 16 bytes long!", 16, initVector.length); + outFile.write(initVector); + + byte[] input = new byte[64]; + int bytesRead; + + while ((bytesRead = inFile.read(input)) != -1) { + byte[] output = cipher.update(input, 0, bytesRead); + if (output != null) + outFile.write(output); + } + + byte[] output = cipher.doFinal(); + if (output != null) + outFile.write(output); + + outFile.flush(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void decrypt(char[] password, byte[] salt, String encryptedFileS, String decryptedFileS) { + + try (FileInputStream fis = new FileInputStream(encryptedFileS); + FileOutputStream fos = new FileOutputStream(decryptedFileS)) { + + decrypt(password, salt, fis, fos); + + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt file " + decryptedFileS + " to " + decryptedFileS, e); + } + + logger.info("Decrypted file " + encryptedFileS + " to " + decryptedFileS); + + } + + public static void decrypt(SecretKey secret, String encryptedFileS, String decryptedFileS) { + + try (FileInputStream fis = new FileInputStream(encryptedFileS); + FileOutputStream fos = new FileOutputStream(decryptedFileS)) { + + decrypt(secret, fis, fos); + + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt file " + decryptedFileS + " to " + decryptedFileS, e); + } + + logger.info("Decrypted file " + encryptedFileS + " to " + decryptedFileS); + + } + + public static void decrypt(char[] password, byte[] salt, InputStream fis, OutputStream fos) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + decrypt(secret, fis, fos); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void decrypt(SecretKey secret, InputStream fis, OutputStream fos) { + + try { + + // read the initialization vector + byte[] initVector = new byte[16]; + fis.read(initVector); + + // init cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(initVector)); + + byte[] in = new byte[64]; + int read; + while ((read = fis.read(in)) != -1) { + byte[] output = cipher.update(in, 0, read); + if (output != null) + fos.write(output); + } + + byte[] output = cipher.doFinal(); + if (output != null) + fos.write(output); + + fos.flush(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] encrypt(char[] password, byte[] salt, String clearText) { + return encrypt(password, salt, clearText.getBytes()); + } + + public static byte[] encrypt(SecretKey secret, byte[] salt, String clearText) { + return encrypt(secret, clearText.getBytes()); + } + + public static byte[] encrypt(char[] password, byte[] salt, byte[] clearTextBytes) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + return encrypt(secret, clearTextBytes); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] encrypt(SecretKey secret, byte[] clearTextBytes) { + + try { + + // set up cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.ENCRYPT_MODE, secret); + + // set up the initialization vector + AlgorithmParameters params = cipher.getParameters(); + byte[] initVector = params.getParameterSpec(IvParameterSpec.class).getIV(); + DBC.INTERIM.assertEquals("IV must be 16 bytes long!", 16, initVector.length); + + // encrypt + byte[] encryptedBytes = cipher.doFinal(clearTextBytes); + + // create result bytes + ByteBuffer byteBuffer = ByteBuffer.allocate(initVector.length + encryptedBytes.length); + byteBuffer.put(initVector); + byteBuffer.put(encryptedBytes); + + return byteBuffer.array(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] decrypt(char[] password, byte[] salt, byte[] encryptedBytes) { + + try { + + // set up key + SecretKey secret = buildSecret(password, salt); + + return decrypt(secret, encryptedBytes); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static byte[] decrypt(SecretKey secret, byte[] encryptedBytes) { + + try { + + // read initialization vector + byte[] initVector = new byte[16]; + System.arraycopy(encryptedBytes, 0, initVector, 0, 16); + + // init cipher + Cipher cipher = Cipher.getInstance(CIPHER); + cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(initVector)); + + byte[] decryptedBytes = cipher.doFinal(encryptedBytes, 16, encryptedBytes.length - 16); + return decryptedBytes; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static SecretKey buildSecret(char[] password, byte[] salt) { + try { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password, salt, 65536, 256); + SecretKey secretKey = factory.generateSecret(keySpec); + SecretKey secret = new SecretKeySpec(secretKey.getEncoded(), "AES"); + + return secret; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java new file mode 100644 index 000000000..797f79d86 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java @@ -0,0 +1,127 @@ +package ch.eitchnet.utils.helper; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import org.junit.Test; + +public class AesCryptoHelperTest { + + private static final char[] password = "A2589309-17AE-4819-B9E4-E577CFA7778F".toCharArray(); + private static final byte[] salt; + + static { + try { + salt = "E68761B3-4E8E-4122-9B12-8B89E0AEB233".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + @Test + public void shouldWrapStreams() throws Exception { + + byte[] clearTextBytes = "Some text".getBytes(); + + // encrypt data + ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream(); + OutputStream outputStream = AesCryptoHelper.wrapEncrypt(password, salt, encryptedOut); + outputStream.write(clearTextBytes); + outputStream.flush(); + outputStream.close(); + + // decrypt data + byte[] encryptedBytes = encryptedOut.toByteArray(); + ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); + InputStream inputStream = AesCryptoHelper.wrapDecrypt(password, salt, encryptedIn); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + byte[] readBuffer = new byte[64]; + int read = 0; + while ((read = inputStream.read(readBuffer)) != -1) { + decryptedOut.write(readBuffer, 0, read); + } + + byte[] decryptedBytes = decryptedOut.toByteArray(); + + assertArrayEquals(clearTextBytes, decryptedBytes); + } + + @Test + public void shouldEncryptBytes() { + + byte[] clearTextBytes = "Some text".getBytes(); + + byte[] encryptedBytes = AesCryptoHelper.encrypt(password, salt, clearTextBytes); + byte[] decryptedBytes = AesCryptoHelper.decrypt(password, salt, encryptedBytes); + + assertArrayEquals(clearTextBytes, decryptedBytes); + } + + @Test + public void shouldEncryptShortFile() { + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_short.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_short.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_short.txt"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + } + + @Test + public void shouldEncryptMiddleFile() { + + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_middle.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_middle.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_middle.txt"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + } + + @Test + public void shouldEncryptLongFile() { + + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_long.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_long.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_long.txt"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + } + + @Test + public void shouldEncryptBinaryFile() { + + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_image.ico"; + // encrypted file + String encryptedFileS = "target/encrypted_image.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_image.ico"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + } + + private static void testCrypto(String clearTextFileS, String encryptedFileS, String decryptedFileS) { + AesCryptoHelper.encrypt(password, salt, clearTextFileS, encryptedFileS); + AesCryptoHelper.decrypt(password, salt, encryptedFileS, decryptedFileS); + + String inputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(clearTextFileS))); + String doutputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(decryptedFileS))); + assertEquals(inputSha256, doutputSha256); + } +} diff --git a/src/test/resources/crypto_test_image.ico b/src/test/resources/crypto_test_image.ico new file mode 100644 index 0000000000000000000000000000000000000000..b48b3442ad90fd9b5bcbef8a9f703c1852de1561 GIT binary patch literal 5430 zcmchb`BM}}6vrn=RerU~e}JE;{1#6HHI^o#f*c}>As#Wtm_$)Ph+G1TXayBe6BCaj zBvFZYti~%*t5QJ(qGiBR5RfGTyDo=F)D_J}U0&Yn+3uO1JtUi$srpv;&d&7rGu>}y z-Wx@ENqJcb3R18QQYuF%O0c3RBe134AHS+74cLDj_u(PRR21!J8}2u50HBb1J8i-H z^GnJy-(Fx}HF~CN<)|6J^~tDkV0r`F0EJ_@GTutC@5?VP>+P@wb6;w&{Zbd38W!W= ze*Rj)LQDa&q=ZI0uKrvT%db0iVypwN8I1DBq5m%sCSss@IAAI*OG0mw(cjx|y|2Ii zaF}QV#&}i&4y?7>yht*>uTqszL3?#w9`!A4Z<>(o%HDl46i*XPO*DN?& zb`b9M^*>*|{m|8FfeVLE!M5e;>^k)8=WDIjjEw|M5!hG7nJyeUiDCNBwxOXRXuNnC zGN#S<8B?4mdE9JB9v2D8$U)Lr)P8jU{+za>sTGn#qWs2o3iBrhy)+)9{~fCxj&9xU z)-G&!!HFGTLyh?iIR3cLtwmc=xLu7YA90j2J_=IC&sK#D#G_}bK6FZqMIB51b;BHK zYnC^A2{PQ1j&H2K>^HVk(O)l>M`QBlFV@Vbk$#>yQ|#@a7WaH<^Ca#$WJ(K(L4T6Q!TZE>@$hMBJPY}= zHPhyZui3gO@i5TcD;-a9;XSrhp0o)uAW873Oh$iOYddUTmabD8?EEMb4i@eJEB>e7 zT+g1t*2Qal#x`-KPly$gINV${hU?dl@2~H2x!_poUca$TYxZgA9H#V8SHqH^P+ore`&~wWw7>02EzsdYeFz}e>EoaI=f0E$P z_mRh&zn)LOX2**62!HCd)YW^$w()+;m^c@N!m!VzF?sqod7UR>I=cBQ$^VYcF3pv< z_|8Osy#xz!AxrJiAEhzd8_?e{N3?E*(aXTzc1t>z)`Q29w>2Z4tV!|cZ?+Yr}0hH@s)ccGAh%fLfy1}1r~v~}ZarnOoeQH&d78-Kfs z=B(%;e{TwmN#l85Y;THLZa=;>VAT__liOn98CMPQolSFYlORrV7lW##0>eLEH9wJJq<#YCS8{-=$+ zs?f6&z3DpzeU=D9iSHEn4a2zMGiBu7wFTzY<073@yA0=hMAFy@=N>$Votw77Eb1}c zw+&tg`5TkQNC}B@vf9*L8;h~rb-R8X-sd$qaj?2hmxH%mxdwj@K9ckh-z`+P6}`KP z)opwnYFU1*tI;~KwxW%4pBuGTptVZ_u;qTt=VdsCFRV4C$s;9-fAMBM=kVy!~kovz!EF=d`RZ&qtT_5G>kr(HMHW9fZ7(A|gi`CXu#m#%$FVv>}5QhmUm z_dLh5y3en~yq)I;2Xc#<|L>S%sCkmRCc!#{J-c=J5kLP>;pZYB+LyhVoj-X0ff$ot zcb1f6%mse_gZ=^N>#)Iqy%(rnppEi2DPO=mTz*H#!&S^Jy4yP?*_tgZU)m74RL&_w z;~;NpqEme~xt~cM?=~b;aDKnO#LS+}hG(*IOzqw1eh=YwAIRQneste4 nvNEsKToAs Date: Fri, 16 Oct 2015 17:21:14 +0200 Subject: [PATCH 164/180] [Minor] removed useless logging statement --- src/main/java/ch/eitchnet/utils/helper/XmlHelper.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java index da2f03efc..15410fd5b 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java @@ -225,8 +225,7 @@ public class XmlHelper { try { String docEncoding = document.getInputEncoding(); - if (docEncoding == null || docEncoding.isEmpty()) { - XmlHelper.logger.info(MessageFormat.format("No encoding passed. Using default encoding {0}", encoding)); //$NON-NLS-1$ + if (StringHelper.isEmpty(docEncoding)) { docEncoding = encoding; } From 1de56d00434c42d20c9b289eb3d43725a97cfed3 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Oct 2015 17:31:58 +0200 Subject: [PATCH 165/180] [New] Handling illegal key size in AesCryptoHelperTest --- .../utils/helper/AesCryptoHelperTest.java | 163 ++++++++++++------ 1 file changed, 111 insertions(+), 52 deletions(-) diff --git a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java index 797f79d86..f8207a12d 100644 --- a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java @@ -9,11 +9,16 @@ import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class AesCryptoHelperTest { + private static final Logger logger = LoggerFactory.getLogger(AesCryptoHelperTest.class); + private static final char[] password = "A2589309-17AE-4819-B9E4-E577CFA7778F".toCharArray(); private static final byte[] salt; @@ -28,92 +33,146 @@ public class AesCryptoHelperTest { @Test public void shouldWrapStreams() throws Exception { - byte[] clearTextBytes = "Some text".getBytes(); + try { + byte[] clearTextBytes = "Some text".getBytes(); - // encrypt data - ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream(); - OutputStream outputStream = AesCryptoHelper.wrapEncrypt(password, salt, encryptedOut); - outputStream.write(clearTextBytes); - outputStream.flush(); - outputStream.close(); + // encrypt data + ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream(); + OutputStream outputStream = AesCryptoHelper.wrapEncrypt(password, salt, encryptedOut); + outputStream.write(clearTextBytes); + outputStream.flush(); + outputStream.close(); - // decrypt data - byte[] encryptedBytes = encryptedOut.toByteArray(); - ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); - InputStream inputStream = AesCryptoHelper.wrapDecrypt(password, salt, encryptedIn); + // decrypt data + byte[] encryptedBytes = encryptedOut.toByteArray(); + ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); + InputStream inputStream = AesCryptoHelper.wrapDecrypt(password, salt, encryptedIn); - ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); - byte[] readBuffer = new byte[64]; - int read = 0; - while ((read = inputStream.read(readBuffer)) != -1) { - decryptedOut.write(readBuffer, 0, read); + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + byte[] readBuffer = new byte[64]; + int read = 0; + while ((read = inputStream.read(readBuffer)) != -1) { + decryptedOut.write(readBuffer, 0, read); + } + + byte[] decryptedBytes = decryptedOut.toByteArray(); + + assertArrayEquals(clearTextBytes, decryptedBytes); + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; } - - byte[] decryptedBytes = decryptedOut.toByteArray(); - - assertArrayEquals(clearTextBytes, decryptedBytes); } @Test public void shouldEncryptBytes() { + try { - byte[] clearTextBytes = "Some text".getBytes(); + byte[] clearTextBytes = "Some text".getBytes(); - byte[] encryptedBytes = AesCryptoHelper.encrypt(password, salt, clearTextBytes); - byte[] decryptedBytes = AesCryptoHelper.decrypt(password, salt, encryptedBytes); + byte[] encryptedBytes = AesCryptoHelper.encrypt(password, salt, clearTextBytes); + byte[] decryptedBytes = AesCryptoHelper.decrypt(password, salt, encryptedBytes); - assertArrayEquals(clearTextBytes, decryptedBytes); + assertArrayEquals(clearTextBytes, decryptedBytes); + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; + } } @Test public void shouldEncryptShortFile() { - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_short.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_short.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_short.txt"; + try { - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_short.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_short.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_short.txt"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; + } } @Test public void shouldEncryptMiddleFile() { + try { - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_middle.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_middle.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_middle.txt"; + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_middle.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_middle.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_middle.txt"; - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; + } } @Test public void shouldEncryptLongFile() { + try { - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_long.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_long.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_long.txt"; + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_long.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_long.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_long.txt"; - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; + } } @Test public void shouldEncryptBinaryFile() { - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_image.ico"; - // encrypted file - String encryptedFileS = "target/encrypted_image.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_image.ico"; + try { - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_image.ico"; + // encrypted file + String encryptedFileS = "target/encrypted_image.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_image.ico"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + + } catch (RuntimeException e) { + if (e.getCause() instanceof InvalidKeyException + && e.getCause().getMessage().equals("Illegal key size or default parameters")) + logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); + else + throw e; + } } private static void testCrypto(String clearTextFileS, String encryptedFileS, String decryptedFileS) { From d0691e4d35220d94d29d642694749524b66fc66c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 16 Oct 2015 17:39:06 +0200 Subject: [PATCH 166/180] [New] Handling illegal key size in AesCryptoHelperTest --- .../utils/helper/ExceptionHelper.java | 16 +++ .../utils/helper/AesCryptoHelperTest.java | 115 +++++++----------- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java index 916489e71..35bb99a6d 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java @@ -95,4 +95,20 @@ public class ExceptionHelper { String root = formatExceptionMessage(t.getCause()); return getExceptionMessage(t) + "\ncause:\n" + root; } + + /** + * Returns the root cause for the given {@link Throwable} + * + * @param t + * the {@link Throwable} for which to get the root cause + * + * @return the root cause of the given {@link Throwable} + */ + public static Throwable getRootCause(Throwable t) { + while (t.getCause() != null) { + t = t.getCause(); + } + + return t; + } } diff --git a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java index f8207a12d..8e6162040 100644 --- a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java @@ -9,7 +9,6 @@ import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.security.InvalidKeyException; import org.junit.Test; import org.slf4j.Logger; @@ -59,8 +58,7 @@ public class AesCryptoHelperTest { assertArrayEquals(clearTextBytes, decryptedBytes); } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) + if (ExceptionHelper.getRootCause(e).getMessage().equals("Illegal key size or default parameters")) logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); else throw e; @@ -78,8 +76,7 @@ public class AesCryptoHelperTest { assertArrayEquals(clearTextBytes, decryptedBytes); } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) + if (ExceptionHelper.getRootCause(e).getMessage().equals("Illegal key size or default parameters")) logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); else throw e; @@ -88,99 +85,69 @@ public class AesCryptoHelperTest { @Test public void shouldEncryptShortFile() { - try { + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_short.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_short.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_short.txt"; - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_short.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_short.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_short.txt"; - - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); - - } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) - logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); - else - throw e; - } + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); } @Test public void shouldEncryptMiddleFile() { - try { + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_middle.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_middle.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_middle.txt"; - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_middle.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_middle.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_middle.txt"; - - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); - - } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) - logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); - else - throw e; - } + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); } @Test public void shouldEncryptLongFile() { - try { + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_long.txt"; + // encrypted file + String encryptedFileS = "target/encrypted_long.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_long.txt"; - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_long.txt"; - // encrypted file - String encryptedFileS = "target/encrypted_long.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_long.txt"; - - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); - - } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) - logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); - else - throw e; - } + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); } @Test public void shouldEncryptBinaryFile() { + // file to be encrypted + String clearTextFileS = "src/test/resources/crypto_test_image.ico"; + // encrypted file + String encryptedFileS = "target/encrypted_image.aes"; + // encrypted file + String decryptedFileS = "target/decrypted_image.ico"; + + testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + + } + + private static void testCrypto(String clearTextFileS, String encryptedFileS, String decryptedFileS) { try { - // file to be encrypted - String clearTextFileS = "src/test/resources/crypto_test_image.ico"; - // encrypted file - String encryptedFileS = "target/encrypted_image.aes"; - // encrypted file - String decryptedFileS = "target/decrypted_image.ico"; + AesCryptoHelper.encrypt(password, salt, clearTextFileS, encryptedFileS); + AesCryptoHelper.decrypt(password, salt, encryptedFileS, decryptedFileS); - testCrypto(clearTextFileS, encryptedFileS, decryptedFileS); + String inputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(clearTextFileS))); + String doutputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(decryptedFileS))); + assertEquals(inputSha256, doutputSha256); } catch (RuntimeException e) { - if (e.getCause() instanceof InvalidKeyException - && e.getCause().getMessage().equals("Illegal key size or default parameters")) + if (ExceptionHelper.getRootCause(e).getMessage().equals("Illegal key size or default parameters")) logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); else throw e; } } - - private static void testCrypto(String clearTextFileS, String encryptedFileS, String decryptedFileS) { - AesCryptoHelper.encrypt(password, salt, clearTextFileS, encryptedFileS); - AesCryptoHelper.decrypt(password, salt, encryptedFileS, decryptedFileS); - - String inputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(clearTextFileS))); - String doutputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(decryptedFileS))); - assertEquals(inputSha256, doutputSha256); - } } From 540dbeab32f542b7f4e3747c191d06c66b5108a9 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 30 Nov 2015 21:26:01 +0100 Subject: [PATCH 167/180] [New] Added StringHelper.replacePropertiesIn() with special prefix - and added a test --- .../eitchnet/utils/helper/StringHelper.java | 31 +++- .../utils/helper/ReplacePropertiesInTest.java | 138 ++++++++++++++++++ 2 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java index 8f82b3487..a7a3fcc94 100644 --- a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/StringHelper.java @@ -385,6 +385,27 @@ public class StringHelper { */ 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; @@ -393,7 +414,7 @@ public class StringHelper { int stop = 0; // loop on $ character positions - while ((pos = tmpValue.indexOf('$', pos + 1)) != -1) { + while ((pos = tmpValue.indexOf(prefix, pos + 1)) != -1) { // if pos+1 is not { character then continue if (tmpValue.charAt(pos + 1) != '{') { @@ -414,9 +435,9 @@ public class StringHelper { String sequence = tmpValue.substring(pos + 2, stop); // make sure sequence doesn't contain $ { } characters - if (sequence.contains("$") || 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}"; //$NON-NLS-1$ - msg = MessageFormat.format(msg, pos, stop, sequence); + 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; @@ -432,7 +453,7 @@ public class StringHelper { } // property exists, so replace in value - tmpValue = tmpValue.replace("${" + sequence + "}", property); //$NON-NLS-1$ //$NON-NLS-2$ + tmpValue = tmpValue.replace(prefixS + "{" + sequence + "}", property); //$NON-NLS-1$ //$NON-NLS-2$ } return tmpValue; diff --git a/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java b/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java new file mode 100644 index 000000000..f9525899d --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java @@ -0,0 +1,138 @@ +package ch.eitchnet.utils.helper; + +import static org.junit.Assert.assertEquals; + +import java.util.Properties; + +import org.junit.Test; + +public class ReplacePropertiesInTest { + + @Test + public void shouldReplaceProps1() { + + String expr = "bla ${foo}"; + String expected = "bla bar"; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps2() { + + String expr = "${foo} bla "; + String expected = "bar bla "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps3() { + + String expr = "bla ${foo} "; + String expected = "bla bar "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps4() { + + String expr = "bla${foo}abr"; + String expected = "blabarabr"; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps5() { + + String expr = "bla '${foo}' "; + String expected = "bla 'bar' "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps6() { + + String expr = "${foo}bla ${foo} "; + String expected = "barbla bar "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps7() { + + String expr = "${foo}bla ${food} "; + String expected = "barbla foofoo "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + properties.setProperty("food", "foofoo"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps8() { + + String expr = "foo"; + String expected = "foo"; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + + String result = StringHelper.replacePropertiesIn(properties, expr); + + assertEquals(expected, result); + } + + @Test + public void shouldReplaceProps9() { + + String expr = "%{foo}bla %{food} "; + String expected = "barbla foofoo "; + + Properties properties = new Properties(); + properties.setProperty("foo", "bar"); + properties.setProperty("food", "foofoo"); + + String result = StringHelper.replacePropertiesIn(properties, '%', expr); + + assertEquals(expected, result); + } +} From 2008da00a357accb2783147acd0b6bd67580f261 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 2 Dec 2015 22:03:22 +0100 Subject: [PATCH 168/180] [Minor] added checking that working dir is a dir in ProcessHelper --- .../java/ch/eitchnet/utils/helper/ProcessHelper.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index a4e3f3294..020422deb 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -40,8 +40,8 @@ public class ProcessHelper { final int[] returnValue = new int[1]; try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - final BufferedReader inputStream = new BufferedReader(new InputStreamReader( - process.getInputStream()));) { + final BufferedReader inputStream = new BufferedReader( + new InputStreamReader(process.getInputStream()));) { Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ @Override @@ -79,8 +79,8 @@ public class ProcessHelper { public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { - if (!workingDirectory.exists()) { - String msg = "Working directory does not exist at {0}"; //$NON-NLS-1$ + if (!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); } @@ -99,8 +99,8 @@ public class ProcessHelper { int[] returnValue = new int[1]; try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - final BufferedReader inputStream = new BufferedReader(new InputStreamReader( - process.getInputStream()));) { + final BufferedReader inputStream = new BufferedReader( + new InputStreamReader(process.getInputStream()));) { Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ @Override From a4b1857b8432ca1a7105cdfd945e597cb6b61dff Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 17:10:14 +0100 Subject: [PATCH 169/180] [Minor] Added a timeout in ProcessHelper --- .../eitchnet/utils/helper/ProcessHelper.java | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 020422deb..40dfba36e 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.text.MessageFormat; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,6 +79,11 @@ public class ProcessHelper { } public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { + return runCommand(1, TimeUnit.MINUTES, workingDirectory, commandAndArgs); + } + + public static ProcessResult runCommand(long timeout, TimeUnit unit, File workingDirectory, + String... commandAndArgs) { if (!workingDirectory.isDirectory()) { String msg = "Working directory does not exist or is not a directory at {0}"; //$NON-NLS-1$ @@ -98,27 +104,19 @@ public class ProcessHelper { final Process process = processBuilder.start(); int[] returnValue = new int[1]; - try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - final BufferedReader inputStream = new BufferedReader( - new InputStreamReader(process.getInputStream()));) { + try (BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); + BufferedReader inputStream = new BufferedReader(new InputStreamReader(process.getInputStream()));) { - Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ - @Override - public void run() { - readStream(sb, "[ERROR] ", errorStream); //$NON-NLS-1$ - } - }; + Thread errorIn = new Thread(() -> readStream(sb, "[ERROR] ", errorStream), "errorIn"); errorIn.start(); - Thread infoIn = new Thread("infoIn") { //$NON-NLS-1$ - @Override - public void run() { - readStream(sb, "[INFO] ", inputStream); //$NON-NLS-1$ - } - }; + Thread infoIn = new Thread(() -> readStream(sb, "[INFO] ", inputStream), "infoIn"); infoIn.start(); - returnValue[0] = process.waitFor(); + boolean ok = process.waitFor(timeout, unit); + if (!ok) + sb.append("[ERROR] Command failed to end before timeout or failed to execute."); + returnValue[0] = process.exitValue(); errorIn.join(100l); infoIn.join(100l); From 041b9ec2e5327ef558a52a6621e76286c4e56629 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 17:26:45 +0100 Subject: [PATCH 170/180] [Minor] Added a timeout in ProcessHelper --- src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 40dfba36e..b1b2906cb 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -20,7 +20,9 @@ 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; @@ -101,6 +103,9 @@ public class ProcessHelper { processBuilder.environment(); processBuilder.directory(workingDirectory); + long start = System.nanoTime(); + logger.info(MessageFormat.format("Starting command (Timeout {0}m) {1}", unit.toMinutes(timeout), + Arrays.stream(commandAndArgs).collect(Collectors.joining(" ")))); final Process process = processBuilder.start(); int[] returnValue = new int[1]; @@ -123,6 +128,7 @@ public class ProcessHelper { 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) { From 3b4d9f366188725e786eceb807bc51d8112e1518 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 18:18:41 +0100 Subject: [PATCH 171/180] [Minor] Added a timeout in ProcessHelper --- .../eitchnet/utils/helper/ProcessHelper.java | 72 ++++++------------- 1 file changed, 21 insertions(+), 51 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index b1b2906cb..680e73fdb 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -34,60 +34,22 @@ public class ProcessHelper { private static final Logger logger = LoggerFactory.getLogger(ProcessHelper.class); - public static ProcessResult runCommand(String command) { - final StringBuffer sb = new StringBuffer(); - sb.append("=====================================\n"); //$NON-NLS-1$ - try { - - final Process process = Runtime.getRuntime().exec(command); - final int[] returnValue = new int[1]; - - try (final BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); - final BufferedReader inputStream = new BufferedReader( - new InputStreamReader(process.getInputStream()));) { - - Thread errorIn = new Thread("errorIn") { //$NON-NLS-1$ - @Override - public void run() { - readStream(sb, "[ERROR] ", errorStream); //$NON-NLS-1$ - } - }; - errorIn.start(); - - Thread infoIn = new Thread("infoIn") { //$NON-NLS-1$ - @Override - public void run() { - readStream(sb, "[INFO] ", inputStream); //$NON-NLS-1$ - } - }; - infoIn.start(); - - returnValue[0] = process.waitFor(); - - errorIn.join(100l); - infoIn.join(100l); - sb.append("=====================================\n"); //$NON-NLS-1$ - } - return new ProcessResult(returnValue[0], sb.toString(), null); - - } catch (IOException e) { - String msg = MessageFormat.format("Failed to perform command: {0}", e.getMessage()); //$NON-NLS-1$ - throw new RuntimeException(msg, e); - } 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 ProcessResult runCommand(String... commandAndArgs) { + return runCommand(null, commandAndArgs); } public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { return runCommand(1, TimeUnit.MINUTES, 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.isDirectory()) { + 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); @@ -99,14 +61,14 @@ public class ProcessHelper { sb.append("=====================================\n"); //$NON-NLS-1$ try { - ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs); - processBuilder.environment(); - processBuilder.directory(workingDirectory); + ProcessBuilder pb = new ProcessBuilder(commandAndArgs); + pb.environment(); + pb.directory(workingDirectory); long start = System.nanoTime(); logger.info(MessageFormat.format("Starting command (Timeout {0}m) {1}", unit.toMinutes(timeout), Arrays.stream(commandAndArgs).collect(Collectors.joining(" ")))); - final Process process = processBuilder.start(); + final Process process = pb.start(); int[] returnValue = new int[1]; try (BufferedReader errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); @@ -120,8 +82,16 @@ public class ProcessHelper { boolean ok = process.waitFor(timeout, unit); if (!ok) - sb.append("[ERROR] Command failed to end before timeout or failed to execute."); - returnValue[0] = process.exitValue(); + 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); From 4e7063ffdebe2e952da60a9e41311c7f44ce812b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 18:19:13 +0100 Subject: [PATCH 172/180] [Minor] Added a timeout in ProcessHelper --- src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 680e73fdb..d779d9492 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -39,7 +39,7 @@ public class ProcessHelper { } public static ProcessResult runCommand(File workingDirectory, String... commandAndArgs) { - return runCommand(1, TimeUnit.MINUTES, workingDirectory, commandAndArgs); + return runCommand(30, TimeUnit.SECONDS, workingDirectory, commandAndArgs); } public static ProcessResult runCommand(long timeout, TimeUnit unit, String... commandAndArgs) { From 9e368bd5c60ac765c2208454fb8a270e2826b749 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 18:21:41 +0100 Subject: [PATCH 173/180] [Minor] Added a timeout in ProcessHelper --- src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index d779d9492..1b8af1325 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -66,8 +66,8 @@ public class ProcessHelper { pb.directory(workingDirectory); long start = System.nanoTime(); - logger.info(MessageFormat.format("Starting command (Timeout {0}m) {1}", unit.toMinutes(timeout), - Arrays.stream(commandAndArgs).collect(Collectors.joining(" ")))); + logger.info(MessageFormat.format("Starting command (Timeout {0} {1}) {2}", unit.toMinutes(timeout), + unit.name(), Arrays.stream(commandAndArgs).collect(Collectors.joining(" ")))); final Process process = pb.start(); int[] returnValue = new int[1]; From 3336cfda1b1311ad43c1625288705573b5ba471b Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 4 Dec 2015 18:24:56 +0100 Subject: [PATCH 174/180] [Minor] Added a timeout in ProcessHelper --- src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java index 1b8af1325..2d84b0624 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java @@ -66,8 +66,8 @@ public class ProcessHelper { pb.directory(workingDirectory); long start = System.nanoTime(); - logger.info(MessageFormat.format("Starting command (Timeout {0} {1}) {2}", unit.toMinutes(timeout), - unit.name(), Arrays.stream(commandAndArgs).collect(Collectors.joining(" ")))); + 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]; From c947260853d57f13cd9c563fa81fa07dd0493d42 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Wed, 10 Feb 2016 20:28:11 +0100 Subject: [PATCH 175/180] [Minor] Code cleanup --- .../communication/ConnectionMode.java | 3 ++ .../communication/SimpleMessageArchive.java | 4 +-- .../ch/eitchnet/db/DbSchemaVersionCheck.java | 7 +++-- src/main/java/ch/eitchnet/utils/Version.java | 27 +++++++---------- .../ch/eitchnet/utils/collections/Paging.java | 2 +- .../utils/helper/ExceptionHelper.java | 13 ++++---- .../ch/eitchnet/utils/helper/FileHelper.java | 4 +-- .../utils/iso8601/ISO8601Duration.java | 30 +++++++++---------- .../utils/iso8601/ISO8601Worktime.java | 18 +++++------ .../utils/objectfilter/ObjectFilter.java | 24 +++++++-------- .../collections/DefaultedHashMapTest.java | 2 +- .../utils/helper/AesCryptoHelperTest.java | 29 ++++++++++-------- .../GenerateReverseBaseEncodingAlphabets.java | 2 +- 13 files changed, 79 insertions(+), 86 deletions(-) diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/src/main/java/ch/eitchnet/communication/ConnectionMode.java index 59d5d89d6..8cf2e2640 100644 --- a/src/main/java/ch/eitchnet/communication/ConnectionMode.java +++ b/src/main/java/ch/eitchnet/communication/ConnectionMode.java @@ -39,6 +39,7 @@ public enum ConnectionMode { * or do any other kind of work */ OFF { + @Override public boolean isSimulation() { return false; } @@ -50,6 +51,7 @@ public enum ConnectionMode { * re-established should an {@link IOException} occur */ ON { + @Override public boolean isSimulation() { return false; } @@ -60,6 +62,7 @@ public enum ConnectionMode { * {@link CommunicationConnection} accepts messages, but silently swallows them, instead of processing them */ SIMULATION { + @Override public boolean isSimulation() { return true; } diff --git a/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java b/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java index 27414f3db..00ad303b6 100644 --- a/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java +++ b/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java @@ -77,7 +77,7 @@ public class SimpleMessageArchive implements IoMessageArchive { @Override public synchronized List getBy(String connectionId) { - List all = new ArrayList(); + List all = new ArrayList<>(); for (IoMessage msg : this.messageArchive) { if (msg.getConnectionId().equals(connectionId)) all.add(msg); @@ -87,7 +87,7 @@ public class SimpleMessageArchive implements IoMessageArchive { @Override public synchronized List getBy(String connectionId, CommandKey key) { - List all = new ArrayList(); + List all = new ArrayList<>(); for (IoMessage msg : this.messageArchive) { if (msg.getConnectionId().equals(connectionId) && msg.getKey().equals(key)) all.add(msg); diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java index d621a4c58..8f870fec0 100644 --- a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java +++ b/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java @@ -162,9 +162,10 @@ public class DbSchemaVersionCheck { Version currentVersion = null; try (PreparedStatement st = con.prepareStatement(sql)) { st.setString(1, app); - ResultSet rs = st.executeQuery(); - if (rs.next()) - currentVersion = Version.valueOf(rs.getString(2)); + try (ResultSet rs = st.executeQuery()) { + if (rs.next()) + currentVersion = Version.valueOf(rs.getString(2)); + } } return currentVersion; diff --git a/src/main/java/ch/eitchnet/utils/Version.java b/src/main/java/ch/eitchnet/utils/Version.java index e3507bbfb..7b6b93dd6 100644 --- a/src/main/java/ch/eitchnet/utils/Version.java +++ b/src/main/java/ch/eitchnet/utils/Version.java @@ -125,14 +125,10 @@ public class Version implements Comparable { * 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) { - if (qualifier == null) { - qualifier = ""; - } - this.major = major; this.minor = minor; this.micro = micro; - this.qualifier = qualifier; + this.qualifier = qualifier == null ? "" : qualifier; this.versionString = null; validate(); } @@ -155,8 +151,8 @@ public class Version implements Comparable { String qual = StringHelper.EMPTY; try { - StringTokenizer st = new StringTokenizer(version, SEPARATOR + MAVEN_QUALIFIER_SEPARATOR - + OSGI_QUALIFIER_SEPARATOR, true); + StringTokenizer st = new StringTokenizer(version, + SEPARATOR + MAVEN_QUALIFIER_SEPARATOR + OSGI_QUALIFIER_SEPARATOR, true); maj = Integer.parseInt(st.nextToken()); if (st.hasMoreTokens()) { // minor @@ -248,16 +244,14 @@ public class Version implements Comparable { * If {@code version} is improperly formatted. */ public static Version valueOf(String version) { - if (version == null) { + if (version == null) return emptyVersion; - } - version = version.trim(); - if (version.length() == 0) { + String trimmedVersion = version.trim(); + if (trimmedVersion.length() == 0) return emptyVersion; - } - return new Version(version); + return new Version(trimmedVersion); } /** @@ -478,12 +472,11 @@ public class Version implements Comparable { private String createQualifier(boolean withOsgiStyle) { if (this.qualifier.equals(MAVEN_SNAPSHOT_QUALIFIER) || this.qualifier.equals(OSGI_SNAPSHOT_QUALIFIER)) { - if (withOsgiStyle) { + if (withOsgiStyle) return OSGI_SNAPSHOT_QUALIFIER; - } else { - return MAVEN_SNAPSHOT_QUALIFIER; - } + return MAVEN_SNAPSHOT_QUALIFIER; } + return this.qualifier; } diff --git a/src/main/java/ch/eitchnet/utils/collections/Paging.java b/src/main/java/ch/eitchnet/utils/collections/Paging.java index 7031c34e4..dde14cd4f 100644 --- a/src/main/java/ch/eitchnet/utils/collections/Paging.java +++ b/src/main/java/ch/eitchnet/utils/collections/Paging.java @@ -89,7 +89,7 @@ public class Paging { */ public static Paging asPage(List list, int pageSize, int page) { - Paging paging = new Paging(pageSize, page); + Paging paging = new Paging<>(pageSize, page); paging.nrOfElements = list.size(); if (paging.pageSize <= 0 || paging.pageToReturn <= 0) { diff --git a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java index 35bb99a6d..a498c20cb 100644 --- a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java @@ -54,10 +54,8 @@ public class ExceptionHelper { * @return */ public static String getExceptionMessageWithCauses(Throwable t) { - - if (t.getCause() == null) { + if (t.getCause() == null) return getExceptionMessage(t); - } String root = getExceptionMessageWithCauses(t.getCause()); return getExceptionMessage(t) + "\n" + root; @@ -87,10 +85,8 @@ public class ExceptionHelper { * @return a string representation of the given {@link Throwable}'s messages including causes */ public static String formatExceptionMessage(Throwable t) { - - if (t.getCause() == null) { + if (t.getCause() == null) return getExceptionMessage(t); - } String root = formatExceptionMessage(t.getCause()); return getExceptionMessage(t) + "\ncause:\n" + root; @@ -99,12 +95,13 @@ public class ExceptionHelper { /** * Returns the root cause for the given {@link Throwable} * - * @param t + * @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 t) { + public static Throwable getRootCause(Throwable throwable) { + Throwable t = throwable; while (t.getCause() != null) { t = t.getCause(); } diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java index 397ed6382..57cf58bc9 100644 --- a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java +++ b/src/main/java/ch/eitchnet/utils/helper/FileHelper.java @@ -378,7 +378,7 @@ public class FileHelper { File file = files.get(i); // get list of parents for this file - List parents = new ArrayList(); + List parents = new ArrayList<>(); File parent = file.getParentFile(); while (parent != null) { parents.add(parent); @@ -389,7 +389,7 @@ public class FileHelper { // and now the same for the next file File fileNext = files.get(i + 1); - List parentsNext = new ArrayList(); + List parentsNext = new ArrayList<>(); File parentNext = fileNext.getParentFile(); while (parentNext != null) { parentsNext.add(parentNext); diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java index 61788c737..61e63f94d 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java @@ -54,9 +54,13 @@ public class ISO8601Duration implements DurationFormat { * @author gattom */ public enum TimeDuration { - SECOND(1000, 'S'), MINUTE(60 * SECOND.duration(), 'M'), HOUR(60 * MINUTE.duration(), 'H'), DAY(24 * HOUR - .duration(), 'D'), WEEK(7 * DAY.duration(), 'W'), MONTH(30 * DAY.duration(), 'M'), YEAR(12 * MONTH - .duration(), 'Y'); + SECOND(1000, 'S'), + MINUTE(60 * SECOND.duration(), 'M'), + HOUR(60 * MINUTE.duration(), 'H'), + DAY(24 * HOUR.duration(), 'D'), + WEEK(7 * DAY.duration(), 'W'), + MONTH(30 * DAY.duration(), 'M'), + YEAR(12 * MONTH.duration(), 'Y'); final long millis; final char isoChar; @@ -74,17 +78,15 @@ public class ISO8601Duration implements DurationFormat { char duration = isostring.charAt(unitIndex); switch (duration) { case 'S': - if (isostring.substring(0, unitIndex).contains("T")) { + if (isostring.substring(0, unitIndex).contains("T")) return SECOND; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); + throw new NumberFormatException( + duration + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); case 'H': - if (isostring.substring(0, unitIndex).contains("T")) { + if (isostring.substring(0, unitIndex).contains("T")) return HOUR; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); + throw new NumberFormatException( + duration + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); case 'D': return DAY; case 'W': @@ -92,11 +94,9 @@ public class ISO8601Duration implements DurationFormat { case 'Y': return YEAR; case 'M': - if (isostring.substring(0, unitIndex).contains("T")) { + if (isostring.substring(0, unitIndex).contains("T")) return MINUTE; - } else { - return MONTH; - } + return MONTH; default: throw new NumberFormatException(duration + " is not a valid unit of time in ISO8601"); } diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java index 31dde2a1c..e107a7be7 100644 --- a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java +++ b/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java @@ -73,17 +73,15 @@ public class ISO8601Worktime implements WorktimeFormat { char duration = isostring.charAt(unitIndex); switch (duration) { case 'S': - if (isostring.substring(0, unitIndex).contains("T")) { + if (isostring.substring(0, unitIndex).contains("T")) return SECOND; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); + throw new NumberFormatException( + duration + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1S)"); case 'H': - if (isostring.substring(0, unitIndex).contains("T")) { + if (isostring.substring(0, unitIndex).contains("T")) return HOUR; - } else - throw new NumberFormatException(duration - + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); + throw new NumberFormatException( + duration + " is not a valid unit of time in ISO8601 without a preceeding T (e.g.: PT1H)"); case 'M': return MINUTE; default: @@ -143,11 +141,9 @@ public class ISO8601Worktime implements WorktimeFormat { } while (newposition < s.length()); return newResult; - - } else { - throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); } + throw new NumberFormatException(s + " cannot be parsed to ISO 8601 Duration"); } /** diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java index b2282e9ca..6ea65d1ac 100644 --- a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java +++ b/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java @@ -88,8 +88,8 @@ public class ObjectFilter { * Default constructor initializing the filter */ public ObjectFilter() { - this.cache = new HashMap(); - this.keySet = new HashSet(); + this.cache = new HashMap<>(); + this.keySet = new HashSet<>(); } /** @@ -439,7 +439,7 @@ public class ObjectFilter { * @return The list of all objects registered under the given key and that need to be added. */ public List getAdded(String key) { - List addedObjects = new LinkedList(); + List addedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { @@ -460,7 +460,7 @@ public class ObjectFilter { * @return The list of all objects registered under the given key and that need to be added. */ public List getAdded(Class clazz, String key) { - List addedObjects = new LinkedList(); + List addedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { @@ -483,7 +483,7 @@ public class ObjectFilter { * @return The list of all objects registered under the given key and that need to be updated. */ public List getUpdated(String key) { - List updatedObjects = new LinkedList(); + List updatedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { @@ -504,7 +504,7 @@ public class ObjectFilter { * @return The list of all objects registered under the given key and that need to be updated. */ public List getUpdated(Class clazz, String key) { - List updatedObjects = new LinkedList(); + List updatedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { @@ -527,7 +527,7 @@ public class ObjectFilter { * @return The list of object registered under the given key that have, as a final action, removal. */ public List getRemoved(String key) { - List removedObjects = new LinkedList(); + List removedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { @@ -548,7 +548,7 @@ public class ObjectFilter { * @return The list of object registered under the given key that have, as a final action, removal. */ public List getRemoved(Class clazz, String key) { - List removedObjects = new LinkedList(); + List removedObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { @@ -573,7 +573,7 @@ public class ObjectFilter { * @return The list of object registered under the given key that have, as a final action, removal. */ public List getAll(Class clazz, String key) { - List objects = new LinkedList(); + List objects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { @@ -596,7 +596,7 @@ public class ObjectFilter { * @return The list of all objects that of the given class */ public List getAll(Class clazz) { - List objects = new LinkedList(); + List objects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getObject().getClass() == clazz) { @@ -618,7 +618,7 @@ public class ObjectFilter { * @return The list of objects matching the given key. */ public List getAll(String key) { - List allObjects = new LinkedList(); + List allObjects = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { @@ -638,7 +638,7 @@ public class ObjectFilter { * @return The list of objects matching the given key. */ public List getCache(String key) { - List allCache = new LinkedList(); + List allCache = new LinkedList<>(); Collection allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { diff --git a/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java b/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java index ddf7c6569..1048d10e1 100644 --- a/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java +++ b/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java @@ -33,7 +33,7 @@ public class DefaultedHashMapTest { @Before public void setUp() { - this.map = new DefaultedHashMap("foobar"); + this.map = new DefaultedHashMap<>("foobar"); this.map.put("foo", "foofoo"); } diff --git a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java index 8e6162040..31b069146 100644 --- a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java @@ -37,26 +37,27 @@ public class AesCryptoHelperTest { // encrypt data ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream(); - OutputStream outputStream = AesCryptoHelper.wrapEncrypt(password, salt, encryptedOut); - outputStream.write(clearTextBytes); - outputStream.flush(); - outputStream.close(); + try (OutputStream outputStream = AesCryptoHelper.wrapEncrypt(password, salt, encryptedOut)) { + outputStream.write(clearTextBytes); + outputStream.flush(); + } // decrypt data byte[] encryptedBytes = encryptedOut.toByteArray(); ByteArrayInputStream encryptedIn = new ByteArrayInputStream(encryptedBytes); - InputStream inputStream = AesCryptoHelper.wrapDecrypt(password, salt, encryptedIn); + try (InputStream inputStream = AesCryptoHelper.wrapDecrypt(password, salt, encryptedIn)) { - ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); - byte[] readBuffer = new byte[64]; - int read = 0; - while ((read = inputStream.read(readBuffer)) != -1) { - decryptedOut.write(readBuffer, 0, read); + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + byte[] readBuffer = new byte[64]; + int read = 0; + while ((read = inputStream.read(readBuffer)) != -1) { + decryptedOut.write(readBuffer, 0, read); + } + + byte[] decryptedBytes = decryptedOut.toByteArray(); + assertArrayEquals(clearTextBytes, decryptedBytes); } - byte[] decryptedBytes = decryptedOut.toByteArray(); - - assertArrayEquals(clearTextBytes, decryptedBytes); } catch (RuntimeException e) { if (ExceptionHelper.getRootCause(e).getMessage().equals("Illegal key size or default parameters")) logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); @@ -75,6 +76,7 @@ public class AesCryptoHelperTest { byte[] decryptedBytes = AesCryptoHelper.decrypt(password, salt, encryptedBytes); assertArrayEquals(clearTextBytes, decryptedBytes); + } catch (RuntimeException e) { if (ExceptionHelper.getRootCause(e).getMessage().equals("Illegal key size or default parameters")) logger.warn("YOU ARE MISSING THE UNLIMITED JCE POLICIES and can not do AES encryption!"); @@ -141,6 +143,7 @@ public class AesCryptoHelperTest { String inputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(clearTextFileS))); String doutputSha256 = StringHelper.getHexString(FileHelper.hashFileSha256(new File(decryptedFileS))); + assertEquals(inputSha256, doutputSha256); } catch (RuntimeException e) { diff --git a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java index 5a492a2d7..e013c5659 100644 --- a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java +++ b/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java @@ -39,7 +39,7 @@ public class GenerateReverseBaseEncodingAlphabets { public static String generateReverseAlphabet(String name, byte[] alphabet) { - Map valueToIndex = new HashMap(); + Map valueToIndex = new HashMap<>(); for (byte i = 0; i < alphabet.length; i++) { Byte value = Byte.valueOf(i); Byte key = Byte.valueOf(alphabet[value]); From 779bdbb3c10d4b2f497e0eb332c201d5fd01dec1 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 3 Mar 2016 14:36:21 +0100 Subject: [PATCH 176/180] [New] Added new XmlDomSigner with tests --- .../eitchnet/utils/helper/XmlDomSigner.java | 245 ++++++++++++++++++ .../utils/helper/XmlSignHelperTest.java | 185 +++++++++++++ src/test/resources/SignedXmlFile.xml | 20 ++ .../resources/SignedXmlFileWithNamespaces.xml | 20 ++ src/test/resources/test.jks | Bin 0 -> 2263 bytes 5 files changed, 470 insertions(+) create mode 100644 src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java create mode 100644 src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java create mode 100644 src/test/resources/SignedXmlFile.xml create mode 100644 src/test/resources/SignedXmlFileWithNamespaces.xml create mode 100644 src/test/resources/test.jks diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java new file mode 100644 index 000000000..bb0bf014e --- /dev/null +++ b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java @@ -0,0 +1,245 @@ +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.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.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; + +public class XmlDomSigner { + + private static final Logger logger = LoggerFactory.getLogger(XmlDomSigner.class); + + private PrivateKeyEntry keyEntry; + private X509Certificate cert; + + public XmlDomSigner(File keyStorePath, String alias, char[] password) { + try { + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new FileInputStream(keyStorePath), password); + this.keyEntry = (PrivateKeyEntry) ks.getEntry(ks.aliases().nextElement(), + new KeyStore.PasswordProtection(password)); + + this.cert = (X509Certificate) this.keyEntry.getCertificate(); + + } catch (Exception e) { + throw new RuntimeException( + "Failed to read certificate and private key from keystore " + keyStorePath + " and alias " + alias); + } + } + + 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); + //Reference ref = fac.newReference("", 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. + + // Create the KeyInfo containing the X509Data. + KeyInfoFactory kif = fac.getKeyInfoFactory(); + List x509Content = new ArrayList<>(); + x509Content.add(this.cert.getSubjectX500Principal().getName()); + x509Content.add(this.cert); + X509Data xd = kif.newX509Data(x509Content); + KeyInfo keyInfo = kif.newKeyInfo(Collections.singletonList(xd)); + + // Create a DOMSignContext and specify the RSA PrivateKey and + // location of the resulting XMLSignature's parent element. + DOMSignContext dsc = new DOMSignContext(this.keyEntry.getPrivateKey(), rootElement); + //dsc.setDefaultNamespacePrefix("samlp"); + dsc.putNamespacePrefix(XMLSignature.XMLNS, "ds"); + + // Create the XMLSignature, but don't sign it yet. + XMLSignature signature = fac.newXMLSignature(signedInfo, keyInfo); + + // Marshal, generate, and sign the enveloped signature. + signature.sign(dsc); + + } catch (Exception e) { + throw new RuntimeException("Failed to sign document", e); + } + } + + public void validate(Document doc) throws RuntimeException { + + try { + + // Create a DOM XMLSignatureFactory that will be used to + // generate the enveloped signature. + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + + // Find Signature element. + NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (nl.getLength() == 0) { + throw new Exception("Cannot find Signature element!"); + } else if (nl.getLength() > 1) { + throw new Exception("Found multiple Signature elements!"); + } + + // Load the KeyStore and get the signing key and certificate. + PublicKey publicKey = this.keyEntry.getCertificate().getPublicKey(); + + // Create a DOMValidateContext and specify a KeySelector + // and document context. + Node signatureNode = nl.item(0); + DOMValidateContext valContext = new DOMValidateContext(publicKey, signatureNode); + valContext.setDefaultNamespacePrefix("samlp"); + valContext.putNamespacePrefix(XMLSignature.XMLNS, "ds"); + valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:protocol", "samlp"); + valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:assertion", "saml"); + valContext.setIdAttributeNS(doc.getDocumentElement(), null, "ID"); + + // Unmarshal the XMLSignature. + valContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + + // Validate the XMLSignature. + boolean coreValidity = signature.validate(valContext); + + // Check core validation status. + if (!coreValidity) { + logger.error("Signature failed core validation"); + boolean sv = signature.getSignatureValue().validate(valContext); + logger.error("signature validation status: " + sv); + if (!sv) { + // Check the validation status of each Reference. + Iterator i = signature.getSignedInfo().getReferences().iterator(); + for (int j = 0; i.hasNext(); j++) { + boolean refValid = ((Reference) i.next()).validate(valContext); + logger.error("ref[" + j + "] validity status: " + refValid); + } + } + throw new RuntimeException("Uh-oh validation, failed!"); + } + + } catch (Exception e) { + if (e instanceof RuntimeException) + throw (RuntimeException) e; + throw new RuntimeException("Failed to validate document", e); + } + } + + public static byte[] transformToBytes(Document doc) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + return out.toByteArray(); + } catch (TransformerFactoryConfigurationError | TransformerException e) { + throw new RuntimeException("Failed to transform document to bytes!", e); + } + } + + public static void writeTo(Document doc, File file) { + try { + writeTo(doc, new FileOutputStream(file)); + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to write document to " + file.getAbsolutePath(), e); + } + } + + public static void writeTo(Document doc, OutputStream out) { + try { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + } catch (Exception e) { + throw new RuntimeException("Failed to write document to output stream!", e); + } + } + + public static Document parse(byte[] bytes) { + return parse(new ByteArrayInputStream(bytes)); + } + + public static Document parse(File signedXmlFile) { + try { + return parse(new FileInputStream(signedXmlFile)); + } catch (Exception e) { + throw new RuntimeException("Failed to parse signed file at " + signedXmlFile.getAbsolutePath(), e); + } + } + + public static Document parse(InputStream in) { + + Document doc; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + doc = dbf.newDocumentBuilder().parse(in); + } catch (Exception e) { + throw new RuntimeException("Failed to parse input stream", e); + } + + return doc; + } +} diff --git a/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java new file mode 100644 index 000000000..561413319 --- /dev/null +++ b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java @@ -0,0 +1,185 @@ +package ch.eitchnet.utils.helper; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone; + +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +public class XmlSignHelperTest { + + private static XmlDomSigner helper; + + @BeforeClass + public static void beforeClass() { + helper = new XmlDomSigner(new File("src/test/resources/test.jks"), "client", "changeit".toCharArray()); + } + + @Test + public void shouldSign() { + Document document = createDoc(); + helper.sign(document); + + assertSignatureElemExists(document); + } + + @Test + public void shouldSignWithNamespaces() { + + Document document = createDocWithNamespaces(); + + // hack for signing with namespaces problem + document = XmlDomSigner.parse(XmlDomSigner.transformToBytes(document)); + + helper.sign(document); + + assertSignatureElemExists(document); + } + + private void assertSignatureElemExists(Document document) { + NodeList nl = document.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals("Expected exactly one Signature element!", 1, nl.getLength()); + } + + @Test + public void shouldValidate() { + + File signedXmlFile = new File("src/test/resources/SignedXmlFile.xml"); + Document document = XmlDomSigner.parse(signedXmlFile); + helper.validate(document); + } + + @Test + public void shouldValidateWithNamespaces() { + + File signedXmlFile = new File("src/test/resources/SignedXmlFileWithNamespaces.xml"); + Document document = XmlDomSigner.parse(signedXmlFile); + helper.validate(document); + } + + @Test + public void shouldSignAndValidate() { + + Document document = createDoc(); + + helper.sign(document); + helper.validate(document); + } + + @Test + public void shouldSignAndValidateWithNamespaces() { + + Document document = createDocWithNamespaces(); + + // hack for signing with namespaces problem + document = XmlDomSigner.parse(XmlDomSigner.transformToBytes(document)); + + helper.sign(document); + helper.validate(document); + } + + public static Document createDoc() { + + String issuer = "test"; + String destination = "test"; + String assertionConsumerServiceUrl = "test"; + Calendar issueInstant = Calendar.getInstance(); + + // create dates + SimpleDateFormat simpleDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + simpleDf.setTimeZone(TimeZone.getTimeZone("UTC")); + String issueInstantS = simpleDf.format(issueInstant.getTime()); + String notBeforeS = issueInstantS; + issueInstant.add(Calendar.SECOND, 10); + String notOnOrAfterS = simpleDf.format(issueInstant.getTime()); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder docBuilder; + try { + docBuilder = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Failed to configure document builder!", e); + } + Document doc = docBuilder.newDocument(); + + Element authnReqE = doc.createElement("AuthnRequest"); + authnReqE.setAttribute("Version", "2.0"); + authnReqE.setAttribute("IssueInstant", issueInstantS); + authnReqE.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + authnReqE.setAttribute("AssertionConsumerServiceURL", assertionConsumerServiceUrl); + authnReqE.setAttribute("Destination", destination); + doc.appendChild(authnReqE); + + Element issuerE = doc.createElement("Issuer"); + issuerE.setTextContent(issuer); + authnReqE.appendChild(issuerE); + + Element conditionsE = doc.createElement("Conditions"); + conditionsE.setAttribute("NotBefore", notBeforeS); + conditionsE.setAttribute("NotOnOrAfter", notOnOrAfterS); + authnReqE.appendChild(conditionsE); + + return doc; + } + + public static Document createDocWithNamespaces() { + + String issuer = "test"; + String destination = "test"; + String assertionConsumerServiceUrl = "test"; + Calendar issueInstant = Calendar.getInstance(); + + // create dates + SimpleDateFormat simpleDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + simpleDf.setTimeZone(TimeZone.getTimeZone("UTC")); + String issueInstantS = simpleDf.format(issueInstant.getTime()); + String notBeforeS = issueInstantS; + issueInstant.add(Calendar.SECOND, 10); + String notOnOrAfterS = simpleDf.format(issueInstant.getTime()); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder docBuilder; + try { + docBuilder = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException("Failed to configure document builder!", e); + } + Document doc = docBuilder.newDocument(); + + Element authnReqE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequest"); + authnReqE.setPrefix("samlp"); + authnReqE.setAttribute("Version", "2.0"); + authnReqE.setAttribute("IssueInstant", issueInstantS); + authnReqE.setAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); + authnReqE.setAttribute("AssertionConsumerServiceURL", assertionConsumerServiceUrl); + authnReqE.setAttribute("Destination", destination); + doc.appendChild(authnReqE); + + Element issuerE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "Issuer"); + issuerE.setPrefix("saml"); + issuerE.setTextContent(issuer); + authnReqE.appendChild(issuerE); + + Element conditionsE = doc.createElementNS("urn:oasis:names:tc:SAML:2.0:assertion", "Conditions"); + conditionsE.setPrefix("saml"); + conditionsE.setAttribute("NotBefore", notBeforeS); + conditionsE.setAttribute("NotOnOrAfter", notOnOrAfterS); + authnReqE.appendChild(conditionsE); + + return doc; + } +} diff --git a/src/test/resources/SignedXmlFile.xml b/src/test/resources/SignedXmlFile.xml new file mode 100644 index 000000000..63197c399 --- /dev/null +++ b/src/test/resources/SignedXmlFile.xml @@ -0,0 +1,20 @@ +test6bxgcmypqGyvyQFgEqdOk1zLTOg=RAWnchzzSHwi84ZJcog6OnkYrGx7rBGBHDsysn1lmP05+AydKzcK7Jw3kbpkGnaaz1DIV/8A/hhA +UmOjwAl8RNygtziXuS5fZfvDUidhPugv2EUKeUkH7CwMkLSB5TONC+AS8eEhfyZbl/4GYMd4Jcqx +OQJgBRMNT6zfybFY+wfJDceFUqCCmyXFgcGBmtSjJqQivwH4B8k1ui49hO67ItCBcCo0aKpqoIxF +UA2IZCDvmrdR/qCq/oA9ssjUzpC+yJMvwPtZ6LDdoWt+MDzDBkCKA6lKUqtU51rZ8GwoRLve8FXH +vCG/ZiqxKn4JwBQL2DiFuZVXhrrMvLpYyi4ZRA==CN=test,OU=ch.eitchnet.utils,O=ch.eitchnet,L=Solothurn,ST=Solothurn,C=CHMIIDizCCAnOgAwIBAgIEPPVdzDANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJDSDESMBAGA1UE +CBMJU29sb3RodXJuMRIwEAYDVQQHEwlTb2xvdGh1cm4xFDASBgNVBAoTC2NoLmVpdGNobmV0MRow +GAYDVQQLExFjaC5laXRjaG5ldC51dGlsczENMAsGA1UEAxMEdGVzdDAeFw0xNjAzMDMxMzEwMTRa +Fw0xNjA2MDExMzEwMTRaMHYxCzAJBgNVBAYTAkNIMRIwEAYDVQQIEwlTb2xvdGh1cm4xEjAQBgNV +BAcTCVNvbG90aHVybjEUMBIGA1UEChMLY2guZWl0Y2huZXQxGjAYBgNVBAsTEWNoLmVpdGNobmV0 +LnV0aWxzMQ0wCwYDVQQDEwR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxW8E +F/odm00xQTRQ95b9RtpmiQo3OQfpkE344vyp48BpOHMm8Q+sjTHde6hGUQQ3FSp6jUZtJBmuXDp+ +hFa4zNp1vleASqmUw4VCZZN1BgHx6coWA1zw/GWyCOFHvr+JTP6/LAfemudOnQVWKx6/NkMEgjNm +OR3qsbfj1km/VlfnIAOG9SvFvydp+Jan7y+nIKllEEXpcQFeiWouvC572aptu9k9qe43mRoQHJ96 +IngJHLONi27SBsF31ipvoHjkYEaTYfv8Izf5wt7h+8tZipFHu0+5r7LbZDRhSWzopC5KZakgVgLG +0JYEzcLotOCf9hmDDZZAAv3OyLoMqN8F0QIDAQABoyEwHzAdBgNVHQ4EFgQUd38xdRA7VcrWcjmz +CYmbBMD4SaUwDQYJKoZIhvcNAQELBQADggEBACqDrXrrYG2sfFBRTIQVni291q8tDqJ1etim1fND +s9ZdYa2oKTaLjMswdlE5hVXtEvRrN+XX3TIAK0lfiDwF4E4JBDww4a3SefEbwPvx110WN6CTE1NI +P6IPCUB7e5QOlg4uKAJoZfnY6HboRiHbeOQxIeis3Q9XpqQSYrO4/NzxFt66m48BHLqf8Hwi90GY +VYMljqr+hHvUTQWGzFD3NKr9Fq6yO2GcHGc5ifmLjwoz2EDAsSubrccbN+RQRRg3II6gFxyL9PYN +HgkGjqdg3v8TiWRxWAFL2MrgNLRzOX9Sl7NMFo6JwiizfLWTgcxZZVIkcU1ZP4heXi5iKUzgsJM= \ No newline at end of file diff --git a/src/test/resources/SignedXmlFileWithNamespaces.xml b/src/test/resources/SignedXmlFileWithNamespaces.xml new file mode 100644 index 000000000..012c1783b --- /dev/null +++ b/src/test/resources/SignedXmlFileWithNamespaces.xml @@ -0,0 +1,20 @@ +testKseWAs8E4H1ZGAbyl2EnlZ3RiG4=KhSDJRxo1u7eNVy1swN5RqA+37oCCeyY8QNCtT1RFz8UVZFqmXGiurscbctKA+tiYSekW4OkxEg9 +Nv03OGJcYlksdZ5CCGlsioac+NY/z2QngtlDaFudKIHwj9yZ9zMdiKT/4kdwnUQP+p9tzYV9GeA9 +gesLOielMdj382XoFQ/CIbrJevE4vpn9FSitbwHXV4kZ3/NxlBPYIgiM9yiTTT0NafFENTS38U+P +k1tL32FcDfHytWN6Twl2ZbHrRYltba/ncxaqkauMA37r9v2f+HS+hXXNluLTazRzAxnhSaetjPOt +GFP/nkG0TcRbiZFTP3YwIHeo94v2d/fs6rKHiQ==CN=test,OU=ch.eitchnet.utils,O=ch.eitchnet,L=Solothurn,ST=Solothurn,C=CHMIIDizCCAnOgAwIBAgIEPPVdzDANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJDSDESMBAGA1UE +CBMJU29sb3RodXJuMRIwEAYDVQQHEwlTb2xvdGh1cm4xFDASBgNVBAoTC2NoLmVpdGNobmV0MRow +GAYDVQQLExFjaC5laXRjaG5ldC51dGlsczENMAsGA1UEAxMEdGVzdDAeFw0xNjAzMDMxMzEwMTRa +Fw0xNjA2MDExMzEwMTRaMHYxCzAJBgNVBAYTAkNIMRIwEAYDVQQIEwlTb2xvdGh1cm4xEjAQBgNV +BAcTCVNvbG90aHVybjEUMBIGA1UEChMLY2guZWl0Y2huZXQxGjAYBgNVBAsTEWNoLmVpdGNobmV0 +LnV0aWxzMQ0wCwYDVQQDEwR0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxW8E +F/odm00xQTRQ95b9RtpmiQo3OQfpkE344vyp48BpOHMm8Q+sjTHde6hGUQQ3FSp6jUZtJBmuXDp+ +hFa4zNp1vleASqmUw4VCZZN1BgHx6coWA1zw/GWyCOFHvr+JTP6/LAfemudOnQVWKx6/NkMEgjNm +OR3qsbfj1km/VlfnIAOG9SvFvydp+Jan7y+nIKllEEXpcQFeiWouvC572aptu9k9qe43mRoQHJ96 +IngJHLONi27SBsF31ipvoHjkYEaTYfv8Izf5wt7h+8tZipFHu0+5r7LbZDRhSWzopC5KZakgVgLG +0JYEzcLotOCf9hmDDZZAAv3OyLoMqN8F0QIDAQABoyEwHzAdBgNVHQ4EFgQUd38xdRA7VcrWcjmz +CYmbBMD4SaUwDQYJKoZIhvcNAQELBQADggEBACqDrXrrYG2sfFBRTIQVni291q8tDqJ1etim1fND +s9ZdYa2oKTaLjMswdlE5hVXtEvRrN+XX3TIAK0lfiDwF4E4JBDww4a3SefEbwPvx110WN6CTE1NI +P6IPCUB7e5QOlg4uKAJoZfnY6HboRiHbeOQxIeis3Q9XpqQSYrO4/NzxFt66m48BHLqf8Hwi90GY +VYMljqr+hHvUTQWGzFD3NKr9Fq6yO2GcHGc5ifmLjwoz2EDAsSubrccbN+RQRRg3II6gFxyL9PYN +HgkGjqdg3v8TiWRxWAFL2MrgNLRzOX9Sl7NMFo6JwiizfLWTgcxZZVIkcU1ZP4heXi5iKUzgsJM= \ No newline at end of file diff --git a/src/test/resources/test.jks b/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0e44e5f08687cfb55bec62c63c664b5fccf208ca GIT binary patch literal 2263 zcmc&#_fwOL63v&U0YWd*tAdpH#Q-rBDM}SYz=UGx1VJg%yGSU~LBUv1`c*&>X%~>L zQiKS|RU}9Ur3nZky*#}0-i-gj`{Dd>&hG5&oZZ>oU)*1WKp@aV0RIB%H zY7dLvK_CbKq{8<=RwR=g5`Y7;P!0e=1HFV)UuN=Q`v3^WlaBI5Jh=+(`p_v-!&!4Ozorij~4dw49uyE zNfDCQ2UTavy5^*ke3IHr#sk7o2ig!rocPfc47K!gOEUS+qP6DKwzOM~&S2u)xSd~A zBMwcCVOns;XJMOpq<;j2uG7cuscHv%dM*(iZ;gDLl%%%&h&BUy``X&$z>%MN5{OwS zxwx;R;%aMXB$m-b}&j-XhUEq7Jq|>F}yBBKqRdE*FY2-i-8Q^d=B~f{T z`NJ)UtF4$wqR9CKw!u?V#oEjNKp&K<@HEN&U4n+wPd&18YU7sSu-KK>;;(1goN6^? zWZ}&Z>ifP2#0#z5qtbQw}YJJ{{J41!UVAy6o=|RbslOPz{@4X|V+7@^a=>MQ0HwS>Hu=f5E3} z9NPkhUm`z5JB*OMb6p7UXe@zhO3YczEbqJ;bst?wrMZTrH-HD(utCSW(8U-d$FqXx z**(3A4ZoQ;i{=*oG8_woo2zIS2V8!#2fFdEnpou6RV_(WX>Rd9FKD~i+0Rwq;@oS3 z?^qg+pRjEaZrCL`@j zWV$TD%{?A3`83IRBEYtbZ9R+Y+6_XIZ(?3Ck4gWp+@+c+rss4~mvFy*ZioV3qV+wR zGG(yPVwmUFyCHHMN2P)>5W|h~+579va6cf=`>3Kpf_D4kWz))M>_g_lDro^38{BkscBW3%|464ai0pJ)4ubIZ`1ffAymj`SsT+H4VJtETb4VqkuEdS zCcHss$PuAyi7Kc_`OdFOrpoLO8&WgPc$OIgJB}qCa_4B`SpybO_28b-at~T~dR5qZ zRoyKtJanNev-^r`*M#y5qvakABl^8BQOWAJf8Gn{UvfNVZAq>p>N_e{YrGK39paAU zUGwLycT|#^n{(BKVhJbS8jNL&UMXqkBNbd}vy{J&W4#mcX?b8?BTfn7?z+2)(Chd{ zCoR9)3$g9FajPe-MXUBfo=Oq`8!$elSl+)A?`l5Ku0&)Fz-APG$b*pldC7i4=Vs3`>ir_ar~iT{QcbAeX(ee z`F9M)1^1))`hg;RXsj}b194a!7Q`xA|4WrY?0@P1tuHDd@ux+H4+KU9*dQPkV1iNs z0Mh0K=i3&}G{$NvUfQ7T=?=NXGvNtG=F*HeCwGgcTHMa~o>*ZkNyd(Z7V4V9@jS92 z$+{j={4dBVH={|l^dbKyi(7_88E;~=De3-5U}dh07e@ZEOQ|?Iq1V(LZ?xYme`NI0 z%*7`NlAK7h@;Nva=Ry#kFRz&zFlZ)O%!t8af628qOS^5-o-LnxCRRjY*O~JMY~!zC z8Ze=ZVvl-;deIX8p#Zy3c8Ej}N~kh9(X$`f8aN>9l@l~=ubb|$vnz?;dObR^(`}V- zN3Y(buB_sVv!a86`)mQmkWwT@g0}b3;O}40R*h$`^GBg+n$W$T&Q~mjV~7t>7ytqC z#X(U}_;8%UtZ-g9cVGn8pIw#MHQ+<2M8#*qTQ&{;`qRY2Jv%fJfXGIbhAh~7l!RR} zHHzlRQfM3~Q((>W4;g&=>F2r10UL+X!jsC0N!?(8DItcq$hmeM|Mm08X^5PGU7Q+X z{2~gj22Par2d@aW?5upY;l<~qbD8UF#Hiyb#rx5rAB_>Q^h+Cx#e2LjDpVaF z3tc6|ZzZNO;RZEZ%H=XkI|T95mvoNd#Zq$kgc8@*(IO~h$}{`X1Fm>yZ%g39VAr@} zl`kQ}?0%&YZ%X`YnaZ&0^hmlD#Z1cE*h(YL))wP<(rEl;dWp<6l)BDA#CLh|u?Z7( JGST@q<3HE1+SUL7 literal 0 HcmV?d00001 From cd5f98225dd5116b3190da5530af19b051e1f449 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 7 Mar 2016 17:43:24 +0100 Subject: [PATCH 177/180] [Major] Changed XmlDomSigner to taked alias names for key finding --- .../eitchnet/utils/helper/XmlDomSigner.java | 51 ++++++++++-------- .../utils/helper/XmlSignHelperTest.java | 23 +++++++- src/test/resources/test.jks | Bin 2263 -> 3205 bytes 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java index bb0bf014e..303d19190 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java @@ -10,6 +10,8 @@ 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; @@ -48,26 +50,33 @@ 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 PrivateKeyEntry keyEntry; - private X509Certificate cert; + private KeyStore keyStore; + private String privateKeyAlias; + private String trustAlias; - public XmlDomSigner(File keyStorePath, String alias, char[] password) { + 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 ks = KeyStore.getInstance("JKS"); - ks.load(new FileInputStream(keyStorePath), password); - this.keyEntry = (PrivateKeyEntry) ks.getEntry(ks.aliases().nextElement(), - new KeyStore.PasswordProtection(password)); - - this.cert = (X509Certificate) this.keyEntry.getCertificate(); + 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 certificate and private key from keystore " + keyStorePath + " and alias " + alias); + throw new RuntimeException("Failed to read keystore " + keyStorePath); } } @@ -93,7 +102,6 @@ public class XmlDomSigner { transforms.add(fac.newTransform(CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null)); DigestMethod digestMethod = fac.newDigestMethod(DigestMethod.SHA1, null); Reference ref = fac.newReference("#" + id, digestMethod, transforms, null, null); - //Reference ref = fac.newReference("", digestMethod, transforms, null, null); // Create the SignedInfo. SignedInfo signedInfo = fac.newSignedInfo( @@ -102,18 +110,23 @@ public class XmlDomSigner { 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 x509Content = new ArrayList<>(); - x509Content.add(this.cert.getSubjectX500Principal().getName()); - x509Content.add(this.cert); + + x509Content.add(cert.getSubjectX500Principal().getName()); + x509Content.add(cert); X509Data xd = kif.newX509Data(x509Content); KeyInfo keyInfo = kif.newKeyInfo(Collections.singletonList(xd)); // Create a DOMSignContext and specify the RSA PrivateKey and // location of the resulting XMLSignature's parent element. - DOMSignContext dsc = new DOMSignContext(this.keyEntry.getPrivateKey(), rootElement); + DOMSignContext dsc = new DOMSignContext(privateKey, rootElement); //dsc.setDefaultNamespacePrefix("samlp"); dsc.putNamespacePrefix(XMLSignature.XMLNS, "ds"); @@ -145,17 +158,13 @@ public class XmlDomSigner { } // Load the KeyStore and get the signing key and certificate. - PublicKey publicKey = this.keyEntry.getCertificate().getPublicKey(); + TrustedCertificateEntry entry = (TrustedCertificateEntry) this.keyStore.getEntry(trustAlias, null); + PublicKey publicKey = entry.getTrustedCertificate().getPublicKey(); // Create a DOMValidateContext and specify a KeySelector // and document context. Node signatureNode = nl.item(0); DOMValidateContext valContext = new DOMValidateContext(publicKey, signatureNode); - valContext.setDefaultNamespacePrefix("samlp"); - valContext.putNamespacePrefix(XMLSignature.XMLNS, "ds"); - valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:protocol", "samlp"); - valContext.putNamespacePrefix("urn:oasis:names:tc:SAML:2.0:assertion", "saml"); - valContext.setIdAttributeNS(doc.getDocumentElement(), null, "ID"); // Unmarshal the XMLSignature. valContext.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE); diff --git a/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java index 561413319..5964354ab 100644 --- a/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java +++ b/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java @@ -24,7 +24,8 @@ public class XmlSignHelperTest { @BeforeClass public static void beforeClass() { - helper = new XmlDomSigner(new File("src/test/resources/test.jks"), "client", "changeit".toCharArray()); + helper = new XmlDomSigner(new File("src/test/resources/test.jks"), "client", "server", + "changeit".toCharArray()); } @Test @@ -58,6 +59,7 @@ public class XmlSignHelperTest { File signedXmlFile = new File("src/test/resources/SignedXmlFile.xml"); Document document = XmlDomSigner.parse(signedXmlFile); + setIdAttr(document); helper.validate(document); } @@ -66,9 +68,28 @@ public class XmlSignHelperTest { File signedXmlFile = new File("src/test/resources/SignedXmlFileWithNamespaces.xml"); Document document = XmlDomSigner.parse(signedXmlFile); + setIdAttrNs(document); + helper.validate(document); } + private void setIdAttr(Document document) { + NodeList authnRequestNodes = document.getElementsByTagName("AuthnRequest"); + if (authnRequestNodes.getLength() != 1) + throw new IllegalStateException("Multiple or no AuthnRequest Node found in document!"); + Element authnRequestNode = (Element) authnRequestNodes.item(0); + authnRequestNode.setIdAttribute("ID", true); + } + + private void setIdAttrNs(Document document) { + NodeList authnRequestNodes = document.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:protocol", + "AuthnRequest"); + if (authnRequestNodes.getLength() != 1) + throw new IllegalStateException("Multiple or no AuthnRequest Node found in document!"); + Element authnRequestNode = (Element) authnRequestNodes.item(0); + authnRequestNode.setIdAttribute("ID", true); + } + @Test public void shouldSignAndValidate() { diff --git a/src/test/resources/test.jks b/src/test/resources/test.jks index 0e44e5f08687cfb55bec62c63c664b5fccf208ca..dfb55f343f49a1ff5ba7b0007e08e739ad17bee6 100644 GIT binary patch delta 68 zcmcaE*ec2M@9n?03=9lRAiR-hJBJ9E!&aPHRF+!Az`z(B`2Ozg&6=#Um_?l95Iq0@ From 49b325eca13ffe7fe9cfc51ab964b8f7decc8522 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Thu, 10 Mar 2016 10:43:52 +0100 Subject: [PATCH 178/180] [Minor] Added method XmlDomSigner.transformToBytes(Document, boolean) --- .../java/ch/eitchnet/utils/helper/XmlDomSigner.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java index 303d19190..175df4922 100644 --- a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java +++ b/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java @@ -36,6 +36,7 @@ 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; @@ -188,7 +189,6 @@ public class XmlDomSigner { } throw new RuntimeException("Uh-oh validation, failed!"); } - } catch (Exception e) { if (e instanceof RuntimeException) throw (RuntimeException) e; @@ -197,10 +197,20 @@ public class XmlDomSigner { } public static byte[] transformToBytes(Document doc) { + return transformToBytes(doc, false); + } + + public static byte[] transformToBytes(Document doc, boolean indent) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); + + if (indent) { + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ + transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$ + } + transformer.transform(new DOMSource(doc), new StreamResult(out)); return out.toByteArray(); } catch (TransformerFactoryConfigurationError | TransformerException e) { From 119336a1fa33628e7f438083844311020b21a179 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 24 Jun 2016 10:16:21 +0200 Subject: [PATCH 179/180] [Project] Moved everything to a sub directory --- LICENSE => ch.eitchnet.utils/LICENSE | 0 README.md => ch.eitchnet.utils/README.md | 0 pom.xml => ch.eitchnet.utils/pom.xml | 0 .../java/ch/eitchnet/communication/CommandKey.java | 0 .../communication/CommunicationConnection.java | 0 .../communication/CommunicationEndpoint.java | 0 .../eitchnet/communication/ConnectionException.java | 0 .../ch/eitchnet/communication/ConnectionInfo.java | 0 .../eitchnet/communication/ConnectionMessages.java | 0 .../ch/eitchnet/communication/ConnectionMode.java | 0 .../eitchnet/communication/ConnectionObserver.java | 0 .../ch/eitchnet/communication/ConnectionState.java | 0 .../communication/ConnectionStateObserver.java | 0 .../java/ch/eitchnet/communication/IoMessage.java | 0 .../ch/eitchnet/communication/IoMessageArchive.java | 0 .../communication/IoMessageStateObserver.java | 0 .../ch/eitchnet/communication/IoMessageVisitor.java | 0 .../communication/SimpleMessageArchive.java | 0 .../communication/StreamMessageVisitor.java | 0 .../java/ch/eitchnet/communication/chat/Chat.java | 0 .../ch/eitchnet/communication/chat/ChatClient.java | 0 .../eitchnet/communication/chat/ChatIoMessage.java | 0 .../communication/chat/ChatMessageVisitor.java | 0 .../ch/eitchnet/communication/chat/ChatServer.java | 0 .../communication/console/ConsoleEndpoint.java | 0 .../console/ConsoleMessageVisitor.java | 0 .../eitchnet/communication/file/FileEndpoint.java | 0 .../communication/file/FileEndpointMode.java | 0 .../communication/tcpip/ClientSocketEndpoint.java | 0 .../communication/tcpip/ServerSocketEndpoint.java | 0 .../tcpip/SocketEndpointConstants.java | 0 .../communication/tcpip/SocketMessageVisitor.java | 0 .../main/java/ch/eitchnet/db/DbConnectionCheck.java | 0 .../src}/main/java/ch/eitchnet/db/DbConstants.java | 0 .../java/ch/eitchnet/db/DbDataSourceBuilder.java | 0 .../src}/main/java/ch/eitchnet/db/DbException.java | 0 .../main/java/ch/eitchnet/db/DbMigrationState.java | 0 .../java/ch/eitchnet/db/DbSchemaVersionCheck.java | 0 .../java/ch/eitchnet/fileserver/FileClient.java | 0 .../java/ch/eitchnet/fileserver/FileClientUtil.java | 0 .../java/ch/eitchnet/fileserver/FileDeletion.java | 0 .../java/ch/eitchnet/fileserver/FileHandler.java | 0 .../main/java/ch/eitchnet/fileserver/FilePart.java | 0 .../java/ch/eitchnet/utils/StringMatchMode.java | 0 .../src}/main/java/ch/eitchnet/utils/Version.java | 0 .../ch/eitchnet/utils/collections/DateRange.java | 0 .../utils/collections/DefaultedHashMap.java | 0 .../ch/eitchnet/utils/collections/MapOfLists.java | 0 .../ch/eitchnet/utils/collections/MapOfMaps.java | 0 .../java/ch/eitchnet/utils/collections/Paging.java | 0 .../java/ch/eitchnet/utils/collections/Tuple.java | 0 .../src}/main/java/ch/eitchnet/utils/dbc/DBC.java | 0 .../ch/eitchnet/utils/exceptions/XmlException.java | 0 .../ch/eitchnet/utils/helper/AesCryptoHelper.java | 0 .../java/ch/eitchnet/utils/helper/ArraysHelper.java | 0 .../java/ch/eitchnet/utils/helper/AsciiHelper.java | 0 .../java/ch/eitchnet/utils/helper/BaseEncoding.java | 0 .../java/ch/eitchnet/utils/helper/ByteHelper.java | 0 .../java/ch/eitchnet/utils/helper/ClassHelper.java | 0 .../main/java/ch/eitchnet/utils/helper/DomUtil.java | 0 .../ch/eitchnet/utils/helper/ExceptionHelper.java | 0 .../java/ch/eitchnet/utils/helper/FileHelper.java | 0 .../java/ch/eitchnet/utils/helper/MathHelper.java | 0 .../ch/eitchnet/utils/helper/ProcessHelper.java | 0 .../ch/eitchnet/utils/helper/PropertiesHelper.java | 0 .../java/ch/eitchnet/utils/helper/StringHelper.java | 0 .../java/ch/eitchnet/utils/helper/SystemHelper.java | 0 .../java/ch/eitchnet/utils/helper/XmlDomSigner.java | 0 .../java/ch/eitchnet/utils/helper/XmlHelper.java | 0 .../ch/eitchnet/utils/io/FileProgressListener.java | 0 .../utils/io/FileStreamProgressWatcher.java | 0 .../utils/io/LoggingFileProgressListener.java | 0 .../utils/io/ProgressableFileInputStream.java | 0 .../java/ch/eitchnet/utils/iso8601/DateFormat.java | 0 .../ch/eitchnet/utils/iso8601/DurationFormat.java | 0 .../ch/eitchnet/utils/iso8601/FormatFactory.java | 0 .../java/ch/eitchnet/utils/iso8601/ISO8601.java | 0 .../ch/eitchnet/utils/iso8601/ISO8601Duration.java | 0 .../utils/iso8601/ISO8601FormatFactory.java | 0 .../ch/eitchnet/utils/iso8601/ISO8601Worktime.java | 0 .../ch/eitchnet/utils/iso8601/WorktimeFormat.java | 0 .../ch/eitchnet/utils/objectfilter/ObjectCache.java | 0 .../eitchnet/utils/objectfilter/ObjectFilter.java | 0 .../ch/eitchnet/utils/objectfilter/Operation.java | 0 .../java/ch/eitchnet/utils/xml/XmlKeyValue.java | 0 .../main/java/javanet/staxutils/Indentation.java | 0 .../javanet/staxutils/IndentingXMLStreamWriter.java | 0 .../staxutils/helpers/StreamWriterDelegate.java | 0 {src => ch.eitchnet.utils/src}/main/java/log4j.xml | 0 .../communication/AbstractEndpointTest.java | 0 .../eitchnet/communication/ConsoleEndpointTest.java | 0 .../ch/eitchnet/communication/FileEndpointTest.java | 0 .../communication/SimpleMessageArchiveTest.java | 0 .../eitchnet/communication/SocketEndpointTest.java | 0 .../communication/TestConnectionObserver.java | 0 .../ch/eitchnet/communication/TestIoMessage.java | 0 .../java/ch/eitchnet/utils/StringMatchModeTest.java | 0 .../test/java/ch/eitchnet/utils/VersionTest.java | 0 .../eitchnet/utils/collections/DateRangeTest.java | 0 .../utils/collections/DefaultedHashMapTest.java | 0 .../ch/eitchnet/utils/collections/PagingTest.java | 0 .../test/java/ch/eitchnet/utils/dbc/DBCTest.java | 0 .../eitchnet/utils/helper/AesCryptoHelperTest.java | 0 .../ch/eitchnet/utils/helper/BaseDecodingTest.java | 0 .../ch/eitchnet/utils/helper/BaseEncodingTest.java | 0 .../eitchnet/utils/helper/ExceptionHelperTest.java | 0 .../GenerateReverseBaseEncodingAlphabets.java | 0 .../utils/helper/ReplacePropertiesInTest.java | 0 .../ch/eitchnet/utils/helper/XmlSignHelperTest.java | 0 .../utils/objectfilter/ObjectFilterTest.java | 0 .../src}/test/resources/SignedXmlFile.xml | 0 .../test/resources/SignedXmlFileWithNamespaces.xml | 0 .../src}/test/resources/crypto_test_image.ico | Bin .../src}/test/resources/crypto_test_long.txt | 0 .../src}/test/resources/crypto_test_middle.txt | 0 .../src}/test/resources/crypto_test_short.txt | 0 .../src}/test/resources/log4j.xml | 0 .../src}/test/resources/test.jks | Bin .../src}/test/resources/test_data.csv | 0 119 files changed, 0 insertions(+), 0 deletions(-) rename LICENSE => ch.eitchnet.utils/LICENSE (100%) rename README.md => ch.eitchnet.utils/README.md (100%) rename pom.xml => ch.eitchnet.utils/pom.xml (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/CommandKey.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/CommunicationConnection.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/CommunicationEndpoint.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionException.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionInfo.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionMessages.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionMode.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionObserver.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionState.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/ConnectionStateObserver.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/IoMessage.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/IoMessageArchive.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/IoMessageStateObserver.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/IoMessageVisitor.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/SimpleMessageArchive.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/StreamMessageVisitor.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/chat/Chat.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/chat/ChatClient.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/chat/ChatServer.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/file/FileEndpoint.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/file/FileEndpointMode.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbConnectionCheck.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbConstants.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbDataSourceBuilder.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbException.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbMigrationState.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/fileserver/FileClient.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/fileserver/FileClientUtil.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/fileserver/FileDeletion.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/fileserver/FileHandler.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/fileserver/FilePart.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/StringMatchMode.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/Version.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/DateRange.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/MapOfLists.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/MapOfMaps.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/Paging.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/collections/Tuple.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/dbc/DBC.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/exceptions/XmlException.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/ArraysHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/AsciiHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/BaseEncoding.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/ByteHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/ClassHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/DomUtil.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/FileHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/MathHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/ProcessHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/StringHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/SystemHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/helper/XmlHelper.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/io/FileProgressListener.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/DateFormat.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/ISO8601.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/objectfilter/Operation.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/javanet/staxutils/Indentation.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/javanet/staxutils/IndentingXMLStreamWriter.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java (100%) rename {src => ch.eitchnet.utils/src}/main/java/log4j.xml (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/AbstractEndpointTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/FileEndpointTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/SocketEndpointTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/TestConnectionObserver.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/communication/TestIoMessage.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/StringMatchModeTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/VersionTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/collections/DateRangeTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/collections/PagingTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/dbc/DBCTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java (100%) rename {src => ch.eitchnet.utils/src}/test/resources/SignedXmlFile.xml (100%) rename {src => ch.eitchnet.utils/src}/test/resources/SignedXmlFileWithNamespaces.xml (100%) rename {src => ch.eitchnet.utils/src}/test/resources/crypto_test_image.ico (100%) rename {src => ch.eitchnet.utils/src}/test/resources/crypto_test_long.txt (100%) rename {src => ch.eitchnet.utils/src}/test/resources/crypto_test_middle.txt (100%) rename {src => ch.eitchnet.utils/src}/test/resources/crypto_test_short.txt (100%) rename {src => ch.eitchnet.utils/src}/test/resources/log4j.xml (100%) rename {src => ch.eitchnet.utils/src}/test/resources/test.jks (100%) rename {src => ch.eitchnet.utils/src}/test/resources/test_data.csv (100%) diff --git a/LICENSE b/ch.eitchnet.utils/LICENSE similarity index 100% rename from LICENSE rename to ch.eitchnet.utils/LICENSE diff --git a/README.md b/ch.eitchnet.utils/README.md similarity index 100% rename from README.md rename to ch.eitchnet.utils/README.md diff --git a/pom.xml b/ch.eitchnet.utils/pom.xml similarity index 100% rename from pom.xml rename to ch.eitchnet.utils/pom.xml diff --git a/src/main/java/ch/eitchnet/communication/CommandKey.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommandKey.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/CommandKey.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommandKey.java diff --git a/src/main/java/ch/eitchnet/communication/CommunicationConnection.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationConnection.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/CommunicationConnection.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationConnection.java diff --git a/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/CommunicationEndpoint.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionException.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionException.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionException.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionException.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionInfo.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionInfo.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionInfo.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionInfo.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMessages.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMessages.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionMessages.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMessages.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMode.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionMode.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionMode.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionObserver.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionObserver.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionObserver.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionState.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionState.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionState.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionState.java diff --git a/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/ConnectionStateObserver.java diff --git a/src/main/java/ch/eitchnet/communication/IoMessage.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessage.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/IoMessage.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessage.java diff --git a/src/main/java/ch/eitchnet/communication/IoMessageArchive.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageArchive.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/IoMessageArchive.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageArchive.java diff --git a/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageStateObserver.java diff --git a/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/IoMessageVisitor.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/IoMessageVisitor.java diff --git a/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/SimpleMessageArchive.java diff --git a/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/StreamMessageVisitor.java diff --git a/src/main/java/ch/eitchnet/communication/chat/Chat.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/Chat.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/chat/Chat.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/Chat.java diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatClient.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatClient.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/chat/ChatClient.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatClient.java diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatIoMessage.java diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatMessageVisitor.java diff --git a/src/main/java/ch/eitchnet/communication/chat/ChatServer.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatServer.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/chat/ChatServer.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/chat/ChatServer.java diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleEndpoint.java diff --git a/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/console/ConsoleMessageVisitor.java diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/file/FileEndpoint.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpoint.java diff --git a/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/file/FileEndpointMode.java diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ClientSocketEndpoint.java diff --git a/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/ServerSocketEndpoint.java diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketEndpointConstants.java diff --git a/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java similarity index 100% rename from src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/communication/tcpip/SocketMessageVisitor.java diff --git a/src/main/java/ch/eitchnet/db/DbConnectionCheck.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConnectionCheck.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbConnectionCheck.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConnectionCheck.java diff --git a/src/main/java/ch/eitchnet/db/DbConstants.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConstants.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbConstants.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbConstants.java diff --git a/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbDataSourceBuilder.java diff --git a/src/main/java/ch/eitchnet/db/DbException.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbException.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbException.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbException.java diff --git a/src/main/java/ch/eitchnet/db/DbMigrationState.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbMigrationState.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbMigrationState.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbMigrationState.java diff --git a/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java similarity index 100% rename from src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/db/DbSchemaVersionCheck.java diff --git a/src/main/java/ch/eitchnet/fileserver/FileClient.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClient.java similarity index 100% rename from src/main/java/ch/eitchnet/fileserver/FileClient.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClient.java diff --git a/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java similarity index 100% rename from src/main/java/ch/eitchnet/fileserver/FileClientUtil.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileClientUtil.java diff --git a/src/main/java/ch/eitchnet/fileserver/FileDeletion.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileDeletion.java similarity index 100% rename from src/main/java/ch/eitchnet/fileserver/FileDeletion.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileDeletion.java diff --git a/src/main/java/ch/eitchnet/fileserver/FileHandler.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileHandler.java similarity index 100% rename from src/main/java/ch/eitchnet/fileserver/FileHandler.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FileHandler.java diff --git a/src/main/java/ch/eitchnet/fileserver/FilePart.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FilePart.java similarity index 100% rename from src/main/java/ch/eitchnet/fileserver/FilePart.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/fileserver/FilePart.java diff --git a/src/main/java/ch/eitchnet/utils/StringMatchMode.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/StringMatchMode.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/StringMatchMode.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/StringMatchMode.java diff --git a/src/main/java/ch/eitchnet/utils/Version.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/Version.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/Version.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/Version.java diff --git a/src/main/java/ch/eitchnet/utils/collections/DateRange.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DateRange.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/DateRange.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DateRange.java diff --git a/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/DefaultedHashMap.java diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/MapOfLists.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfLists.java diff --git a/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/MapOfMaps.java diff --git a/src/main/java/ch/eitchnet/utils/collections/Paging.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/Paging.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/Paging.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/Paging.java diff --git a/src/main/java/ch/eitchnet/utils/collections/Tuple.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/Tuple.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/collections/Tuple.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/collections/Tuple.java diff --git a/src/main/java/ch/eitchnet/utils/dbc/DBC.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/dbc/DBC.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/dbc/DBC.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/dbc/DBC.java diff --git a/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/exceptions/XmlException.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/exceptions/XmlException.java diff --git a/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/AesCryptoHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ArraysHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/AsciiHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/BaseEncoding.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/ByteHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ByteHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/ClassHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ClassHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/DomUtil.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/DomUtil.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/DomUtil.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/DomUtil.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ExceptionHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/FileHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/FileHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/FileHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/FileHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/MathHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/MathHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/MathHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/MathHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/ProcessHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/PropertiesHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/StringHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/StringHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/StringHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/StringHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/SystemHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/SystemHelper.java diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlDomSigner.java diff --git a/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/helper/XmlHelper.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/helper/XmlHelper.java diff --git a/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/io/FileProgressListener.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/FileProgressListener.java diff --git a/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/FileStreamProgressWatcher.java diff --git a/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/LoggingFileProgressListener.java diff --git a/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/io/ProgressableFileInputStream.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/DateFormat.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/DurationFormat.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/FormatFactory.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Duration.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601FormatFactory.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/ISO8601Worktime.java diff --git a/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/iso8601/WorktimeFormat.java diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/ObjectCache.java diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/ObjectFilter.java diff --git a/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/objectfilter/Operation.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/objectfilter/Operation.java diff --git a/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java b/ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java similarity index 100% rename from src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java rename to ch.eitchnet.utils/src/main/java/ch/eitchnet/utils/xml/XmlKeyValue.java diff --git a/src/main/java/javanet/staxutils/Indentation.java b/ch.eitchnet.utils/src/main/java/javanet/staxutils/Indentation.java similarity index 100% rename from src/main/java/javanet/staxutils/Indentation.java rename to ch.eitchnet.utils/src/main/java/javanet/staxutils/Indentation.java diff --git a/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java b/ch.eitchnet.utils/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java similarity index 100% rename from src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java rename to ch.eitchnet.utils/src/main/java/javanet/staxutils/IndentingXMLStreamWriter.java diff --git a/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java b/ch.eitchnet.utils/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java similarity index 100% rename from src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java rename to ch.eitchnet.utils/src/main/java/javanet/staxutils/helpers/StreamWriterDelegate.java diff --git a/src/main/java/log4j.xml b/ch.eitchnet.utils/src/main/java/log4j.xml similarity index 100% rename from src/main/java/log4j.xml rename to ch.eitchnet.utils/src/main/java/log4j.xml diff --git a/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/AbstractEndpointTest.java diff --git a/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/ConsoleEndpointTest.java diff --git a/src/test/java/ch/eitchnet/communication/FileEndpointTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/FileEndpointTest.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/FileEndpointTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/FileEndpointTest.java diff --git a/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/SimpleMessageArchiveTest.java diff --git a/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/SocketEndpointTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/SocketEndpointTest.java diff --git a/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/TestConnectionObserver.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/TestConnectionObserver.java diff --git a/src/test/java/ch/eitchnet/communication/TestIoMessage.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/TestIoMessage.java similarity index 100% rename from src/test/java/ch/eitchnet/communication/TestIoMessage.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/communication/TestIoMessage.java diff --git a/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/StringMatchModeTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/StringMatchModeTest.java diff --git a/src/test/java/ch/eitchnet/utils/VersionTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/VersionTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/VersionTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/VersionTest.java diff --git a/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/DateRangeTest.java diff --git a/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/DefaultedHashMapTest.java diff --git a/src/test/java/ch/eitchnet/utils/collections/PagingTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/PagingTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/collections/PagingTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/collections/PagingTest.java diff --git a/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/dbc/DBCTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/dbc/DBCTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/AesCryptoHelperTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/BaseDecodingTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/BaseEncodingTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/ExceptionHelperTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/GenerateReverseBaseEncodingAlphabets.java diff --git a/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/ReplacePropertiesInTest.java diff --git a/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/helper/XmlSignHelperTest.java diff --git a/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java b/ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java similarity index 100% rename from src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java rename to ch.eitchnet.utils/src/test/java/ch/eitchnet/utils/objectfilter/ObjectFilterTest.java diff --git a/src/test/resources/SignedXmlFile.xml b/ch.eitchnet.utils/src/test/resources/SignedXmlFile.xml similarity index 100% rename from src/test/resources/SignedXmlFile.xml rename to ch.eitchnet.utils/src/test/resources/SignedXmlFile.xml diff --git a/src/test/resources/SignedXmlFileWithNamespaces.xml b/ch.eitchnet.utils/src/test/resources/SignedXmlFileWithNamespaces.xml similarity index 100% rename from src/test/resources/SignedXmlFileWithNamespaces.xml rename to ch.eitchnet.utils/src/test/resources/SignedXmlFileWithNamespaces.xml diff --git a/src/test/resources/crypto_test_image.ico b/ch.eitchnet.utils/src/test/resources/crypto_test_image.ico similarity index 100% rename from src/test/resources/crypto_test_image.ico rename to ch.eitchnet.utils/src/test/resources/crypto_test_image.ico diff --git a/src/test/resources/crypto_test_long.txt b/ch.eitchnet.utils/src/test/resources/crypto_test_long.txt similarity index 100% rename from src/test/resources/crypto_test_long.txt rename to ch.eitchnet.utils/src/test/resources/crypto_test_long.txt diff --git a/src/test/resources/crypto_test_middle.txt b/ch.eitchnet.utils/src/test/resources/crypto_test_middle.txt similarity index 100% rename from src/test/resources/crypto_test_middle.txt rename to ch.eitchnet.utils/src/test/resources/crypto_test_middle.txt diff --git a/src/test/resources/crypto_test_short.txt b/ch.eitchnet.utils/src/test/resources/crypto_test_short.txt similarity index 100% rename from src/test/resources/crypto_test_short.txt rename to ch.eitchnet.utils/src/test/resources/crypto_test_short.txt diff --git a/src/test/resources/log4j.xml b/ch.eitchnet.utils/src/test/resources/log4j.xml similarity index 100% rename from src/test/resources/log4j.xml rename to ch.eitchnet.utils/src/test/resources/log4j.xml diff --git a/src/test/resources/test.jks b/ch.eitchnet.utils/src/test/resources/test.jks similarity index 100% rename from src/test/resources/test.jks rename to ch.eitchnet.utils/src/test/resources/test.jks diff --git a/src/test/resources/test_data.csv b/ch.eitchnet.utils/src/test/resources/test_data.csv similarity index 100% rename from src/test/resources/test_data.csv rename to ch.eitchnet.utils/src/test/resources/test_data.csv From 1a4f2ee0b89e2d9492933d45ac05f28d05cbcb4c Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Fri, 24 Jun 2016 10:21:55 +0200 Subject: [PATCH 180/180] [Project] Removed .gitignore in prep for merge --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 18d2ca6cd..000000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target/ -.classpath -.project -.settings/
  • + * This class implements the encoding part of RFC 4648 https://tools.ietf.org/html/rfc4648. For the decoding see + * {@link BaseDecoding}. + *

    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 z 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