/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.util;
import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.Feature;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.message.Identity;
import de.dal33t.powerfolder.net.ConnectionListener;
import de.dal33t.powerfolder.transfer.Download;
import de.dal33t.powerfolder.util.os.OSUtil;
import de.dal33t.powerfolder.util.os.Win32.ShellLink;
import de.dal33t.powerfolder.util.os.Win32.WinUtils;
/**
* Util helper class.
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.64 $
*/
public class Util {
/** Flag if awt is available */
private static boolean awtAvailable;
// Initalize awt check
static {
// Okay lets check if we have an AWT system
try {
Color col = Color.RED;
col.brighter();
SimpleAttributeSet warn = new SimpleAttributeSet();
StyleConstants.setForeground(warn, Color.RED);
// Okay we have AWT
awtAvailable = true;
} catch (Error e) {
// ERROR ? Okay no AWT
awtAvailable = false;
}
}
private static final Logger LOG = Logger.getLogger(Util.class.getName());
/**
* Used building output as Hex
*/
private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6',
'7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
// No instance possible
private Util() {
}
/**
* Answers if we have the AWT libs available
*
* @return
*/
public static boolean isAwtAvailable() {
return awtAvailable;
}
public static final boolean equals(Object a, Object b) {
if (a == null) {
// a == null
return b == null;
}
if (b == null) {
// a != null
return false;
}
if (a == b) {
return true;
}
return a.equals(b);
}
public static final boolean equalsIgnoreCase(String a, String b) {
if (equals(a, b)) {
return true;
}
return a.equalsIgnoreCase(b);
}
/**
* @param relativeNameA
* @param relativeNameB
* @return true if the relative names are equals on this system. Respects
* {@link FileInfo#IGNORE_CASE}
*/
public static final boolean equalsRelativeName(String relativeNameA,
String relativeNameB)
{
if (relativeNameA == null) {
// a == null
return relativeNameB == null;
}
if (relativeNameB == null) {
// a != null
return false;
}
if (relativeNameA == relativeNameB) {
return true;
}
if (FileInfo.IGNORE_CASE) {
return relativeNameA.equalsIgnoreCase(relativeNameB);
} else {
return relativeNameA.equals(relativeNameB);
}
}
/**
* Safe-get for null char arrays to String conversion.
*
* @param chars
* @return the chars as string. If the input is null output will be null
*/
public static final String toString(char[] chars) {
if (chars == null) {
return null;
}
return new String(chars);
}
/**
* Safe-get for null Strings to char array conversion.
*
* @param str
* @return String as char array. If the input is null output will be null
*/
public static final char[] toCharArray(String str) {
if (str == null) {
return null;
}
return str.toCharArray();
}
/**
* @param email
* the email string to check.
* @return true if the input is a valid email address.
*/
public static boolean isValidEmail(String email) {
if (email == null) {
return false;
}
int etIndex = email.indexOf('@');
boolean orderOk = etIndex > 0 && email.lastIndexOf('.') > etIndex;
if (!orderOk) {
return false;
}
if (email.trim().contains(" ")) {
// Whitespaces not allowed
return false;
}
return true;
}
/**
* @return the line feed characters depending on the system architecture.
*/
public static String getLineFeed() {
String lf = System.getProperty("line.separator");
if (StringUtils.isNotBlank(lf)) {
return lf;
}
if (OSUtil.isWindowsSystem()) {
return "\r\n";
} else {
return "\n";
}
}
public static boolean isMySelfPowerFolderComCloud(Controller controller) {
// WHAT A MESS
return Feature.CREDITS_SYSTEM.isEnabled();
}
/**
* @param c
* @param otherIsOnLAN
* @return true, if this client may request parts from multiple sources
*/
private static boolean allowSwarming(Controller c, boolean otherIsOnLAN) {
Reject.ifNull(c, "Controller is null");
return (ConfigurationEntry.USE_SWARMING_ON_INTERNET.getValueBoolean(c) && !otherIsOnLAN)
|| (ConfigurationEntry.USE_SWARMING_ON_LAN.getValueBoolean(c) && otherIsOnLAN);
}
/**
* @param c
* @param otherIsOnLAN
* @return true, if this client may request parts and a file parts record
*/
private static boolean allowDeltaSync(Controller c, boolean otherIsOnLAN) {
Reject.ifNull(c, "Controller is null");
return (ConfigurationEntry.USE_DELTA_ON_INTERNET.getValueBoolean(c) && !otherIsOnLAN)
|| (ConfigurationEntry.USE_DELTA_ON_LAN.getValueBoolean(c) && otherIsOnLAN);
}
public static boolean useSwarming(Controller c, Member other) {
Reject.ifNull(c, "Controller is null");
Reject.ifNull(other, "other is null!");
Identity id = other.getIdentity();
if (id == null) {
return false;
}
return id.isSupportingPartTransfers()
&& allowSwarming(c, other.isOnLAN());
}
public static boolean useDeltaSync(Controller c, Download d) {
Validate.notNull(c);
if (d.getFile().getSize() < Constants.DELTA_SYNC_MIN_FILESIZE) {
return false;
}
return allowDeltaSync(c, d.getPartner().isOnLAN());
}
/**
* Retrieves the URL to an resource within PF.
*
* @param res
* the filename of the resource
* @param altLocation
* possible alternative (root is tried first) location (directory
* structure like etc/files)
* @return the URL to the resource or null if not possible
*/
public static URL getResource(String res, String altLocation) {
URL result = Thread.currentThread().getContextClassLoader()
.getResource(res);
if (result == null) {
result = Thread.currentThread().getContextClassLoader()
.getResource(altLocation + '/' + res);
}
if (result == null) {
LOG.severe("Unable to load resource " + res + ". alt location "
+ altLocation);
}
return result;
}
/**
* @return The created file or null if resource not found
* @param resource
* filename of the resource
* @param altLocation
* possible alternative (root is tried first) location (directory
* structure like etc/files)
* @param destinationFile
* The file to create
* @param forceOverwrite
* true if the resource should be copied even if not required.
*/
public static File copyResourceTo(String resource, String altLocation,
File destinationFile, boolean forceOverwrite, boolean quiet)
{
Reject.ifNull(resource, "Resoucse");
try {
// Step 1) Check existence of resource
URL resURL = Thread.currentThread().getContextClassLoader()
.getResource(resource);
if (resURL == null && altLocation != null) {
LOG.finer("Unable to find resource: " + resource);
// try harder
resURL = Thread.currentThread().getContextClassLoader()
.getResource(altLocation + '/' + resource);
}
if (resURL == null) {
LOG.fine("Unable to find resource: " + altLocation + "/"
+ resource);
return null;
}
URLConnection resCon = resURL.openConnection();
long lastMod = resCon.getLastModified();
long length = resCon.getContentLength();
// Step 2) Check if update/overwrite is required
if (!forceOverwrite && destinationFile.exists()) {
boolean upToDate = length == destinationFile.length()
&& DateUtil.equalsFileDateCrossPlattform(lastMod,
destinationFile.lastModified());
if (upToDate) {
// No update required
LOG.fine("Not required to update " + resURL + " to "
+ destinationFile);
return destinationFile;
}
}
// Step 3) Actually copy
InputStream in = resCon.getInputStream();
destinationFile.mkdirs();
FileUtils.copyFromStreamToFile(in, destinationFile);
// Preserver last mod for later caching.
destinationFile.setLastModified(resCon.getLastModified());
} catch (IOException ioe) {
if (quiet) {
LOG.fine("Unable to create target for resource: "
+ destinationFile);
} else {
LOG.warning("Unable to create target for resource: "
+ destinationFile);
}
return null;
}
LOG.finer("created target for resource: " + destinationFile);
return destinationFile;
}
public static boolean isDesktopShortcut(String shortcutName) {
WinUtils util = WinUtils.getInstance();
if (util == null) {
return false;
}
File scut = new File(util.getSystemFolderPath(WinUtils.CSIDL_DESKTOP,
false), shortcutName + Constants.LINK_EXTENSION);
return scut.exists();
}
/**
* Creates a desktop shortcut. currently only available on windows systems
*
* @param shortcutName
* @param shortcutTarget
* @return true if succeeded
*/
public static boolean createDesktopShortcut(String shortcutName,
File shortcutTarget)
{
WinUtils util = WinUtils.getInstance();
if (util == null) {
return false;
}
LOG.finer("Creating desktop shortcut to "
+ shortcutTarget.getAbsolutePath());
ShellLink link = new ShellLink(null, shortcutName,
shortcutTarget.getAbsolutePath(), null);
File scut = new File(util.getSystemFolderPath(WinUtils.CSIDL_DESKTOP,
false), shortcutName + Constants.LINK_EXTENSION);
try {
util.createLink(link, scut.getAbsolutePath());
return true;
} catch (IOException e) {
LOG.warning("Couldn't create shortcut " + scut.getAbsolutePath());
LOG.log(Level.FINER, "IOException", e);
}
return false;
}
/**
* Removes a desktop shortcut. currently only available on windows systems
*
* @param shortcutName
* @return true if succeeded
*/
public static boolean removeDesktopShortcut(String shortcutName) {
WinUtils util = WinUtils.getInstance();
if (util == null) {
return false;
}
LOG.finer("Removing desktop shortcut: " + shortcutName);
File scut = new File(util.getSystemFolderPath(WinUtils.CSIDL_DESKTOP,
false), shortcutName + Constants.LINK_EXTENSION);
return scut.delete();
}
/**
* Returns the plain url content as string
*
* @param url
* @return
*/
public static String getURLContent(URL url) {
if (url == null) {
throw new NullPointerException("URL is null");
}
try {
Object content = url.getContent();
if (!(content instanceof InputStream)) {
LOG.severe("Unable to get content from " + url
+ ". content is of type " + content.getClass().getName());
return null;
}
InputStream in = (InputStream) content;
StringBuilder buf = new StringBuilder();
while (in.available() > 0) {
buf.append((char) in.read());
}
return buf.toString();
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to get content from " + url + ". "
+ e.toString(), e);
}
return null;
}
/**
* Place a String on the clipboard
*
* @param aString
* the string to place in the clipboard
*/
public static void setClipboardContents(String aString) {
StringSelection stringSelection = new StringSelection(aString);
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, new ClipboardOwner() {
public void lostOwnership(Clipboard aClipboard,
Transferable contents)
{
// Ignore
}
});
}
/**
* Retrieve a String on the clipboard.
*/
public static String getClipboardContents() {
String result = "";
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable contents = clipboard.getContents(null);
boolean hasTransferableText = contents != null
&& contents.isDataFlavorSupported(DataFlavor.stringFlavor);
if (hasTransferableText) {
try {
result = (String) contents
.getTransferData(DataFlavor.stringFlavor);
} catch (UnsupportedFlavorException ex) {
LOG.severe(ex.getMessage());
} catch (IOException ex) {
LOG.severe(ex.getMessage());
}
}
return result;
}
/**
* Encodes a url fragment, thus special characters are tranferred into a url
* compatible style
*
* @param aURLFragment
* @return the string encoded for URL usage.
*/
public static String endcodeForURL(String aURLFragment) {
String result = null;
try {
// FIX1: Corrected relative filenames including path separator /
result = URLEncoder.encode(aURLFragment, "UTF-8").replace("%2F",
"/");
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("UTF-8 not supported", ex);
}
return result;
}
/**
* Removes the last '/' from an URI and trims the string. Example:
* http://www.powerfolder.com/ gets converted into
* http://www.powerfolder.com
*
* @param uri
* the URI to trim and remove last slash from
* @return the new URI string
*/
public static String removeLastSlashFromURI(String uri) {
if (uri == null) {
return null;
}
String newURI = uri.trim();
if (newURI.endsWith("/") && !newURI.endsWith("://")) {
// Remove last '/' if existing
newURI = newURI.substring(0, newURI.length() - 1);
}
return newURI;
}
/**
* compares two ip addresses.
*
* @param ip1
* @param ip2
* @return true if different, false otherwise.
*/
public static boolean compareIpAddresses(byte[] ip1, byte[] ip2) {
for (int i = 0; i < ip1.length; i++) {
if (ip1[i] != ip2[i]) {
return true;
}
}
return false;
}
/**
* Splits an array into a list of smaller arrays with the maximum size of
* <code>size</code>.
*
* @param src
* the source array
* @param size
* the maximum size of the chunks
* @return the list of resulting arrays
*/
public static List<byte[]> splitArray(byte[] src, int size) {
int nChunks = src.length / size;
List<byte[]> chunkList = new ArrayList<byte[]>(nChunks + 1);
if (size >= src.length) {
chunkList.add(src);
return chunkList;
}
for (int i = 0; i < nChunks; i++) {
byte[] chunk = new byte[size];
System.arraycopy(src, i * size, chunk, 0, chunk.length);
chunkList.add(chunk);
}
int lastChunkSize = src.length % size;
if (lastChunkSize > 0) {
byte[] lastChunk = new byte[lastChunkSize];
System.arraycopy(src, nChunks * size, lastChunk, 0,
lastChunk.length);
chunkList.add(lastChunk);
}
return chunkList;
}
/**
* Merges a list of arrays into one big array.
*
* @param arrayList
* the list of byte[] arryas.
* @return the resulting array.
*/
public static byte[] mergeArrayList(List<byte[]> arrayList) {
Reject.ifNull(arrayList, "list of arrays is null");
int totalSize = 0;
for (byte[] bs : arrayList) {
totalSize += bs.length;
}
if (totalSize == 0) {
return new byte[0];
}
byte[] result = new byte[totalSize];
int pos = 0;
for (byte[] bs : arrayList) {
System.arraycopy(bs, 0, result, pos, bs.length);
pos += bs.length;
}
return result;
}
/**
* Converts an array of bytes into an array of characters representing the
* hexidecimal values of each byte in order. The returned array will be
* double the length of the passed array, as it takes two characters to
* represent any given byte.
*
* @param data
* a byte[] to convert to Hex characters
* @return A char[] containing hexidecimal characters
*/
public static char[] encodeHex(byte[] data) {
int l = data.length;
char[] out = new char[l << 1];
// two characters form the hex value.
for (int i = 0, j = 0; i < l; i++) {
out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
out[j++] = DIGITS[0x0F & data[i]];
}
return out;
}
// Encoding stuff *********************************************************
/**
* Calculates the MD5 digest and returns the value as a 16 element
* <code>byte[]</code>.
*
* @param data
* Data to digest
* @return MD5 digest
*/
public static byte[] md5(byte[] data) {
return getMd5Digest().digest(data);
}
/**
* Returns a MessageDigest for the given <code>algorithm</code>.
*
* @param algorithm
* The MessageDigest algorithm name.
* @return An MD5 digest instance.
* @throws RuntimeException
* when a {@link java.security.NoSuchAlgorithmException} is
* caught,
*/
private static MessageDigest getDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* Returns an MD5 MessageDigest.
*
* @return An MD5 digest instance.
* @throws RuntimeException
* when a {@link java.security.NoSuchAlgorithmException} is
* caught,
*/
private static MessageDigest getMd5Digest() {
return getDigest("MD5");
}
/**
* Checks if a program version is between (including border versions) two
* versions
*
* @param lowVersion
* @param compareVersion
* @param highVersion
* @return
*/
public static boolean betweenVersion(String lowVersion,
String compareVersion, String highVersion)
{
Reject.ifNull(lowVersion, "lowVersion is null");
Reject.ifNull(compareVersion, "compareVersion is null");
Reject.ifNull(highVersion, "highVersion is null");
boolean isBetween = !compareVersions(lowVersion, compareVersion);
if (!isBetween) {
return false;
}
return compareVersions(highVersion, compareVersion);
}
/**
* Comparse two version string which have the format "x.x.x aaa".
* <p>
* The last " aaa" is optional.
*
* @param higherVersion
* @param compareVersion
* @return true if higherVersion is greater than compareVersion
*/
public static boolean compareVersions(String higherVersion,
String compareVersion)
{
Reject.ifNull(higherVersion, "higherVersion is null");
Reject.ifNull(compareVersion, "compareVersion is null");
higherVersion = higherVersion.trim();
compareVersion = compareVersion.trim();
String addition1 = "";
int addStart1 = higherVersion.indexOf(' ');
if (addStart1 >= 0) {
// Get addition text "x.x.x additionaltext"
addition1 = higherVersion.substring(addStart1 + 1,
higherVersion.length());
higherVersion = higherVersion.substring(0, addStart1);
}
StringTokenizer nizer1 = new StringTokenizer(higherVersion, ".");
int major1 = 0;
try {
major1 = Integer.valueOf(nizer1.nextToken());
} catch (Exception e) {
}
int minor1 = 0;
try {
minor1 = Integer.valueOf(nizer1.nextToken());
} catch (Exception e) {
// e.printStackTrace();
}
int bugfix1 = 0;
try {
bugfix1 = Integer.valueOf(nizer1.nextToken());
} catch (Exception e) {
}
String addition2 = "";
int addStart2 = compareVersion.indexOf(' ');
if (addStart2 >= 0) {
// Get addition text "x.x.x additionaltext"
addition2 = compareVersion.substring(addStart2 + 1,
compareVersion.length());
compareVersion = compareVersion.substring(0, addStart2);
}
StringTokenizer nizer2 = new StringTokenizer(compareVersion, ".");
int major2 = 0;
try {
major2 = Integer.valueOf(nizer2.nextToken());
} catch (Exception e) {
}
int minor2 = 0;
try {
minor2 = Integer.valueOf(nizer2.nextToken());
} catch (Exception e) {
}
int bugfix2 = 0;
try {
bugfix2 = Integer.valueOf(nizer2.nextToken());
} catch (Exception e) {
}
// Actually check
if (major1 == major2) {
if (minor1 == minor2) {
if (bugfix1 == bugfix2) {
return addition1.length() < addition2.length();
}
return bugfix1 > bugfix2;
}
return minor1 > minor2;
}
return major1 > major2;
}
/**
* Interprets a string as connection string and returns the address. null is
* returns if parse failed. Format is expeced as ' <connect ip>' or '
* <connect ip>: <port>'
*
* @param connectStr
* The connectStr to parse
* @return a InetSocketAddress created based on the connecStr
*/
public static InetSocketAddress parseConnectionString(String connectStr) {
if (connectStr == null) {
return null;
}
String ip = connectStr.trim();
int remotePort = ConnectionListener.DEFAULT_PORT;
// format <ip/dns> or <ip/dns>:<port> expected
// e.g. localhost:544
int dotdot = connectStr.indexOf(':');
if (dotdot >= 0 && dotdot < connectStr.length()) {
ip = connectStr.substring(0, dotdot);
try {
remotePort = Integer.parseInt(connectStr.substring(dotdot + 1,
connectStr.length()));
} catch (NumberFormatException e) {
LOG.warning("Illegal port in " + connectStr
+ ", trying default port");
}
}
// try to connect
return new InetSocketAddress(ip, remotePort);
}
/**
* Replace every occurences of a string within a string
*
* @param target
* @param from
* @param to
* @return
*/
public static String replace(String target, String from, String to) {
int start = target.indexOf(from);
if (start == -1) {
return target;
}
int lf = from.length();
char[] targetChars = target.toCharArray();
StringBuilder buffer = new StringBuilder();
int copyFrom = 0;
while (start != -1) {
buffer.append(targetChars, copyFrom, start - copyFrom);
buffer.append(to);
copyFrom = start + lf;
start = target.indexOf(from, copyFrom);
}
buffer.append(targetChars, copyFrom, targetChars.length - copyFrom);
return buffer.toString();
}
/**
* Creates a concurrent map with lesser segements to save memory. The
* default concurrency of the maps are 4 (instead of 16 default).
* <p>
* 4 should be more suitable value for in-powerfolder us and procudes lesser
* Segements.
*
* @param <K>
* @param <V>
* @return the concurrent hashmap
*/
public static final <K, V> ConcurrentHashMap<K, V> createConcurrentHashMap()
{
return createConcurrentHashMap(16);
}
/**
* Creates a concurrent map with lesser segements to save memory. The
* default concurrency of the maps are 4 (instead of 16 default).
* <p>
* 4 should be more suitable value for in-powerfolder us and procudes lesser
* Segements.
*
* @param <K>
* @param <V>
* @param intialSize
* the initial size of the map.
* @return the concurrent hashmap
*/
public static final <K, V> ConcurrentHashMap<K, V> createConcurrentHashMap(
int intialSize)
{
return new ConcurrentHashMap<K, V>(intialSize, 0.75f, 4);
}
}