http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLevelConverter.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLevelConverter.java
 
b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLevelConverter.java
new file mode 100644
index 0000000..14bbd4c
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLevelConverter.java
@@ -0,0 +1,99 @@
+/**
+ * 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 org.apache.aurora.common.logging.julbridge;
+
+import java.util.logging.Level;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * An utility class to convert between JUL and Log4j Levels. Mapping is as 
follows:
+ * <ul>
+ * <li>FINEST <-> TRACE</li>
+ * <li>FINER -> DEBUG</li>
+ * <li>FINE <-> DEBUG</li>
+ * <li>INFO <-> INFO</li>
+ * <li>WARNING <-> WARN</li>
+ * <li>SEVERE <-> ERROR</li>
+ * <li>SEVERE <- FATAL</li>
+ * </ul>
+ *
+ * Unknowns levels are mapped to FINE/DEBUG
+ */
+public class JULBridgeLevelConverter {
+
+  private JULBridgeLevelConverter() {}
+
+  /**
+   * Converts a JUL level into a Log4j level.
+   *
+   * @param level the JUL level to convert
+   * @return a Log4j level
+   * @throws NullPointerException if level is null
+   */
+  public static org.apache.log4j.Level toLog4jLevel(Level level) {
+    checkNotNull(level);
+
+    if (level == Level.FINEST) {
+      return org.apache.log4j.Level.TRACE;
+    } else if (level == Level.FINER) {
+      return org.apache.log4j.Level.DEBUG;
+    } else if (level == Level.FINE) {
+      return org.apache.log4j.Level.DEBUG;
+    } else if (level == Level.INFO) {
+      return org.apache.log4j.Level.INFO;
+    } else if (level == Level.WARNING) {
+      return org.apache.log4j.Level.WARN;
+    } else if (level == Level.SEVERE) {
+      return org.apache.log4j.Level.ERROR;
+    } else if (level == Level.ALL) {
+      return org.apache.log4j.Level.ALL;
+    } else if (level == Level.OFF) {
+      return org.apache.log4j.Level.OFF;
+    }
+
+    return org.apache.log4j.Level.DEBUG;
+  }
+
+  /**
+   * Converts a Log4j level into a JUL level.
+   *
+   * @param level the Log4j level to convert
+   * @return a JUL level
+   * @throws NullPointerException if level is null
+   */
+  public static Level fromLog4jLevel(org.apache.log4j.Level level) {
+    checkNotNull(level);
+
+    if (level == org.apache.log4j.Level.TRACE) {
+      return Level.FINEST;
+    } else if (level == org.apache.log4j.Level.DEBUG) {
+      return Level.FINE;
+    } else if (level == org.apache.log4j.Level.INFO) {
+      return Level.INFO;
+    } else if (level == org.apache.log4j.Level.WARN) {
+      return Level.WARNING;
+    } else if (level == org.apache.log4j.Level.ERROR) {
+      return Level.SEVERE;
+    } else if (level == org.apache.log4j.Level.FATAL) {
+      return Level.SEVERE;
+    } else if (level == org.apache.log4j.Level.ALL) {
+      return Level.ALL;
+    } else if (level == org.apache.log4j.Level.OFF) {
+      return Level.OFF;
+    }
+
+    return Level.FINE;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLogManager.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLogManager.java
 
b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLogManager.java
new file mode 100644
index 0000000..adf1a83
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeLogManager.java
@@ -0,0 +1,87 @@
+/**
+ * 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 org.apache.aurora.common.logging.julbridge;
+
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+import org.apache.log4j.spi.LoggerRepository;
+
+/**
+ * A JUL LogManager which takes over the logging configuration and redirects 
all messages to Log4j.
+ *
+ * The approach is inspired by the apache-jul-log4j-bridge project from Paul 
Smith
+ * (<psm...@apache.org>) and available at <a
+ * 
href="http://people.apache.org/~psmith/logging.apache.org/sandbox/jul-log4j-bridge/";
 />
+ *
+ * During initialization, it resets configuration and adds a default handler 
to the root logger to
+ * perform the redirection. It also sets the root logger level to the Log4j 
repository threshold.
+ * This implies that Log4j is properly configured before this manager is 
taking over.
+ *
+ * To install this log manager, simply add the following property to the java 
command line:
+ * 
<code>-Djava.util.logging.manager=com.twitter.common.logging.julbridge.JULBridgeLogManager</code>
+ *
+ * It is possible to configure using extended location information (source 
filename and line info)
+ * by adding the following property to the java command line:
+ * 
<code>-Dcom.twitter.common.logging.julbridge.use-extended-location-info=true</code>
+ *
+ */
+public final class JULBridgeLogManager extends LogManager {
+  /**
+   * System property name to control if log messages sent from JUL to log4j 
should contain
+   * extended location information.
+   *
+   * Set @value to true to add source filename and line number to each message.
+   */
+  public static final String USE_EXTENDED_LOCATION_INFO_PROPERTYNAME =
+      "com.twitter.common.logging.julbridge.use-extended-location-info";
+
+  /*
+   * LogManager requires a public no-arg constructor to be present so a new 
instance can be created
+   * when configured using the system property. A private constructor will 
throw an exception.
+   */
+  public JULBridgeLogManager() {}
+
+  @Override
+  public void readConfiguration() {
+    assimilate(org.apache.log4j.LogManager.getLoggerRepository());
+  }
+
+  /**
+   * Assimilates an existing JUL log manager. Equivalent to calling
+   * {@link #assimilate(LoggerRepository)} with 
<code>LogManager.getLoggerRepository</code>.
+   */
+  public static void assimilate() {
+    assimilate(org.apache.log4j.LogManager.getLoggerRepository());
+  }
+
+  /**
+   * Assimilates an existing JUL log manager.
+   *
+   * It resets the manager configuration, and adds a bridge handler to the 
root logger. Messages are
+   * redirected to the specified Log4j logger repository.
+   *
+   * @param loggerRepository the Log4j logger repository to use to redirect 
messages
+   */
+  public static void assimilate(LoggerRepository loggerRepository) {
+    LogManager.getLogManager().reset();
+
+    boolean withExtendedLocationInfos =
+        Boolean.getBoolean(USE_EXTENDED_LOCATION_INFO_PROPERTYNAME);
+
+    Logger rootLogger = Logger.getLogger("");
+    
rootLogger.setLevel(JULBridgeLevelConverter.fromLog4jLevel(loggerRepository.getThreshold()));
+    rootLogger.addHandler(new JULBridgeHandler(loggerRepository, 
withExtendedLocationInfos));
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/log4j/GlogLayout.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/logging/log4j/GlogLayout.java 
b/commons/src/main/java/org/apache/aurora/common/logging/log4j/GlogLayout.java
new file mode 100644
index 0000000..1a90ded
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/aurora/common/logging/log4j/GlogLayout.java
@@ -0,0 +1,98 @@
+/**
+ * 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 org.apache.aurora.common.logging.log4j;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.aurora.common.logging.Glog;
+import org.apache.log4j.Layout;
+import org.apache.log4j.Level;
+import org.apache.log4j.spi.LocationInfo;
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
+
+/**
+ * Log4j Layout to match the format generated by glog.
+ *
+ * @see Glog
+ */
+public class GlogLayout extends Layout implements Glog.Formatter<LoggingEvent> 
{
+
+  private static final ImmutableMap<Level, Glog.Level> LEVEL_LABELS =
+      ImmutableMap.<Level, Glog.Level>builder()
+          .put(Level.TRACE, Glog.Level.DEBUG)
+          .put(Level.DEBUG, Glog.Level.DEBUG)
+          .put(Level.INFO, Glog.Level.INFO)
+          .put(Level.WARN, Glog.Level.WARNING)
+          .put(Level.ERROR, Glog.Level.ERROR)
+          .put(Level.FATAL, Glog.Level.FATAL)
+          .build();
+
+  @Override
+  public String format(LoggingEvent record) {
+    return Glog.formatRecord(this, record);
+  }
+
+  @Override
+  public boolean ignoresThrowable() {
+    return false; // We handle stack trace formatting.
+  }
+
+  @Override
+  public void activateOptions() {
+    // We use no options
+  }
+
+  @Override
+  public String getMessage(LoggingEvent record) {
+    return record.getRenderedMessage();
+  }
+
+  @Override
+  public String getClassName(LoggingEvent record) {
+    LocationInfo locationInformation = record.getLocationInformation();
+    return (locationInformation != null)
+        ? locationInformation.getClassName()
+        : null;
+  }
+
+  @Override
+  public String getMethodName(LoggingEvent record) {
+    LocationInfo locationInformation = record.getLocationInformation();
+    return (locationInformation != null)
+        ? record.getLocationInformation().getMethodName()
+        : null;
+  }
+
+  @Override
+  public Glog.Level getLevel(LoggingEvent record) {
+    return LEVEL_LABELS.get(record.getLevel());
+  }
+
+  @Override
+  public long getTimeStamp(LoggingEvent record) {
+    return record.getTimeStamp();
+  }
+
+  @Override
+  public long getThreadId(LoggingEvent record) {
+    return Thread.currentThread().getId();
+  }
+
+  @Override
+  public Throwable getThrowable(LoggingEvent record) {
+    ThrowableInformation throwableInformation = 
record.getThrowableInformation();
+    return throwableInformation != null ? throwableInformation.getThrowable() 
: null;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/Environment.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/Environment.java 
b/commons/src/main/java/org/apache/aurora/common/net/Environment.java
new file mode 100644
index 0000000..65fd15e
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/Environment.java
@@ -0,0 +1,47 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+/**
+ * Represents a network environment at the granularity of a datacenter.
+ *
+ * @author John Sirois
+ */
+public interface Environment {
+
+  /**
+   * Returns the name of this network environment's datacenter.
+   *
+   * @return the name of this environment's datacenter
+   */
+  String dcName();
+
+  /**
+   * Creates a fully qualified hostname for a given unqualified hostname in 
the network
+   * environment's datacenter.  Does not confirm that the host exists.
+   *
+   * @param hostname The simple hostname to qualify.
+   * @return The fully qualified hostname.
+   */
+  String fullyQualify(String hostname);
+
+  /**
+   * Checks if a given {@code hostname} is a valid hostname for a host in this 
network environment;
+   * does not guarantee that the host exists in this network environment.
+   *
+   * @param hostname The simple hostname to check for membership in this 
network environment.
+   * @return {@code true} if the hostname is a valid hostname for this network 
environment.
+   */
+  boolean contains(String hostname);
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/InetSocketAddressHelper.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/InetSocketAddressHelper.java
 
b/commons/src/main/java/org/apache/aurora/common/net/InetSocketAddressHelper.java
new file mode 100644
index 0000000..7e88b27
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/aurora/common/net/InetSocketAddressHelper.java
@@ -0,0 +1,136 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang.StringUtils;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Set;
+
+/**
+ * A utility that can parse [host]:[port] pairs or :[port] designators into 
instances of
+ * {@link java.net.InetSocketAddress}. The literal '*' can be specified for 
port as an alternative
+ * to '0' to indicate any local port.
+ *
+ * @author John Sirois
+ */
+public final class InetSocketAddressHelper {
+
+  /**
+   * A function that uses {@link #parse(String)} to map an endpoint spec to an
+   * {@link InetSocketAddress}.
+   */
+  public static final Function<String, InetSocketAddress> STR_TO_INET =
+      new Function<String, InetSocketAddress>() {
+        @Override public InetSocketAddress apply(String value) {
+          return parse(value);
+        }
+      };
+
+  /**
+   * A function that uses {@link #getLocalAddress(int)} to map a local port 
number to an
+   * {@link InetSocketAddress}.
+   * If an {@link UnknownHostException} is thrown, it will be propagated as a
+   * {@link RuntimeException}.
+   */
+  public static final Function<Integer, InetSocketAddress> INT_TO_INET =
+      new Function<Integer, InetSocketAddress>() {
+        @Override public InetSocketAddress apply(Integer port) {
+          try {
+            return getLocalAddress(port);
+          } catch (UnknownHostException e) {
+            throw Throwables.propagate(e);
+          }
+        }
+      };
+
+  public static final Function<InetSocketAddress, String> INET_TO_STR =
+      new Function<InetSocketAddress, String>() {
+        @Override public String apply(InetSocketAddress addr) {
+          return InetSocketAddressHelper.toString(addr);
+        }
+      };
+
+  /**
+   * Attempts to parse an endpoint spec into an InetSocketAddress.
+   *
+   * @param value the endpoint spec
+   * @return a parsed InetSocketAddress
+   * @throws NullPointerException     if {@code value} is {@code null}
+   * @throws IllegalArgumentException if {@code value} cannot be parsed
+   */
+  public static InetSocketAddress parse(String value) {
+    Preconditions.checkNotNull(value);
+
+    String[] spec = value.split(":", 2);
+    if (spec.length != 2) {
+      throw new IllegalArgumentException("Invalid socket address spec: " + 
value);
+    }
+
+    String host = spec[0];
+    int port = asPort(spec[1]);
+
+    return StringUtils.isEmpty(host)
+        ? new InetSocketAddress(port)
+        : InetSocketAddress.createUnresolved(host, port);
+  }
+
+  /**
+   * Attempts to return a usable String given an InetSocketAddress.
+   *
+   * @param value the InetSocketAddress.
+   * @return the String representation of the InetSocketAddress.
+   */
+  public static String toString(InetSocketAddress value) {
+    Preconditions.checkNotNull(value);
+    return value.getHostName() + ":" + value.getPort();
+  }
+
+  private static int asPort(String port) {
+    if ("*".equals(port)) {
+      return 0;
+    }
+    try {
+      return Integer.parseInt(port);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("Invalid port: " + port, e);
+    }
+  }
+
+  public static InetSocketAddress getLocalAddress(int port) throws 
UnknownHostException {
+    String ipAddress = InetAddress.getLocalHost().getHostAddress();
+    return new InetSocketAddress(ipAddress, port);
+  }
+
+  private InetSocketAddressHelper() {
+    // utility
+  }
+
+  /**
+   * Converts backend definitions (in host:port form) a set of socket 
addresses.
+   *
+   * @param backends Backends to convert.
+   * @return Sockets representing the provided backends.
+   */
+  public static Set<InetSocketAddress> convertToSockets(Iterable<String> 
backends) {
+    return Sets.newHashSet(Iterables.transform(backends, STR_TO_INET));
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/ProxyAuthorizer.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/ProxyAuthorizer.java 
b/commons/src/main/java/org/apache/aurora/common/net/ProxyAuthorizer.java
new file mode 100644
index 0000000..c5126dc
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/ProxyAuthorizer.java
@@ -0,0 +1,41 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.net.HttpURLConnection;
+
+/**
+ * Authorizes http connection for use over the proxy it is built with
+ *
+ * @author William Farner
+ */
+public class ProxyAuthorizer {
+  private final ProxyConfig config;
+
+  private ProxyAuthorizer(ProxyConfig config) {
+    this.config = config;
+  }
+
+  public static ProxyAuthorizer adapt(ProxyConfig config) {
+    return new ProxyAuthorizer(config);
+  }
+
+  public void authorize(HttpURLConnection httpCon) {
+    httpCon.setRequestProperty("Proxy-Authorization", "Basic " +
+        new String(Base64.encodeBase64(new String(config.getProxyUser() + ":" +
+          config.getProxyPassword()).getBytes())).trim());
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/ProxyConfig.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/ProxyConfig.java 
b/commons/src/main/java/org/apache/aurora/common/net/ProxyConfig.java
new file mode 100644
index 0000000..fa474b5
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/ProxyConfig.java
@@ -0,0 +1,30 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import java.net.InetSocketAddress;
+import javax.annotation.Nullable;
+
+/**
+ * Proxy configuration parameters: proxy address, username, and password.
+ *
+ * @author John Corwin
+ */
+public interface ProxyConfig {
+  public InetSocketAddress getProxyAddress();
+
+  public @Nullable String getProxyUser();
+
+  public @Nullable String getProxyPassword();
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/UrlHelper.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/UrlHelper.java 
b/commons/src/main/java/org/apache/aurora/common/net/UrlHelper.java
new file mode 100644
index 0000000..3f06d63
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/UrlHelper.java
@@ -0,0 +1,156 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author William Farner
+ */
+public class UrlHelper {
+
+  private static final Logger LOG = 
Logger.getLogger(UrlHelper.class.getName());
+
+  /**
+   * Gets the domain from {@code url}.
+   *
+   * @param url A url.
+   * @return The domain portion of the URL, or {@code null} if the url is 
invalid.
+   */
+  public static String getDomain(String url) {
+    try {
+      return getDomainChecked(url);
+    } catch (URISyntaxException e) {
+      LOG.finest("Malformed url: " + url);
+      return null;
+    }
+  }
+
+  /**
+   * Gets the domain from {@code uri}, and throws an exception if it's not a 
valid uri.
+   *
+   * @param url A url.
+   * @throws URISyntaxException if url is not a valid {@code URI}
+   * @return The domain portion of the given url, or {@code null} if the host 
is undefined.
+   */
+  public static String getDomainChecked(String url) throws URISyntaxException {
+    Preconditions.checkNotNull(url);
+    url = addProtocol(url);
+    return new URI(url).getHost();
+  }
+
+  /**
+   * Gets the path from {@code url}.
+   *
+   * @param url A url.
+   * @return The path portion of the URL, or {@code null} if the url is 
invalid.
+   */
+  public static String getPath(String url) {
+    Preconditions.checkNotNull(url);
+    url = addProtocol(url);
+    try {
+      return new URI(url).getPath();
+    } catch (URISyntaxException e) {
+      LOG.info("Malformed url: " + url);
+      return null;
+    }
+  }
+
+  /**
+   * Strips URL parameters from a url.
+   * This will remove anything after and including a question mark in the URL.
+   *
+   * @param url The URL to strip parameters from.
+   * @return The original URL with parameters stripped, which will be the 
original URL if no
+   *   parameters were found.
+   */
+  public static String stripUrlParameters(String url) {
+    Preconditions.checkNotNull(url);
+    int paramStartIndex = url.indexOf("?");
+    if (paramStartIndex == -1) {
+      return url;
+    } else {
+      return url.substring(0, paramStartIndex);
+    }
+  }
+
+  /**
+   * Convenience method that calls #stripUrlParameters(String) for a URL.
+   *
+   * @param url The URL to strip parameters from.
+   * @return The original URL with parameters stripped, which will be the 
original URL if no
+   *   parameters were found.
+   */
+  public static String stripUrlParameters(URL url) {
+    return stripUrlParameters(url.toString());
+  }
+
+  private static final Pattern URL_PROTOCOL_REGEX =
+      Pattern.compile("^https?://", Pattern.CASE_INSENSITIVE);
+
+  /**
+   * Checks whether a URL specifies its protocol, prepending http if it does 
not.
+   *
+   * @param url The URL to fix.
+   * @return The URL with the http protocol specified if no protocol was 
already specified.
+   */
+  public static String addProtocol(String url) {
+    Preconditions.checkNotNull(url);
+    Matcher matcher = URL_PROTOCOL_REGEX.matcher(url);
+    if (!matcher.find()) {
+      url = "http://"; + url;
+    }
+    return url;
+  }
+
+  /**
+   * Gets the domain levels for a host.
+   * For example, sub1.sub2.domain.co.uk would return
+   * [sub1.sub2.domain.co.uk, sub2.domain.co.uk, domain.co.uk, co.uk, uk].
+   *
+   *
+   * @param host The host to peel subdomains off from.
+   * @return The domain levels in this host.
+   */
+  public static List<String> getDomainLevels(String host) {
+    Preconditions.checkNotNull(host);
+
+    // Automatically include www prefix if not present.
+    if (!host.startsWith("www")) {
+      host = "www." + host;
+    }
+
+    Joiner joiner = Joiner.on(".");
+    List<String> domainParts = 
Lists.newLinkedList(Arrays.asList(host.split("\\.")));
+    List<String> levels = Lists.newLinkedList();
+
+    while (!domainParts.isEmpty()) {
+      levels.add(joiner.join(domainParts));
+      domainParts.remove(0);
+    }
+
+    return levels;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/UrlResolver.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/UrlResolver.java 
b/commons/src/main/java/org/apache/aurora/common/net/UrlResolver.java
new file mode 100644
index 0000000..96c5f07
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/UrlResolver.java
@@ -0,0 +1,446 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Functions;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFutureTask;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+import org.apache.aurora.common.base.ExceptionalFunction;
+import org.apache.aurora.common.net.UrlResolver.ResolvedUrl.EndState;
+import org.apache.aurora.common.quantity.Amount;
+import org.apache.aurora.common.quantity.Time;
+import org.apache.aurora.common.stats.PrintableHistogram;
+import org.apache.aurora.common.util.BackoffStrategy;
+import org.apache.aurora.common.util.Clock;
+import org.apache.aurora.common.util.TruncatedBinaryBackoff;
+import org.apache.aurora.common.util.caching.Cache;
+import org.apache.aurora.common.util.caching.LRUCache;
+
+/**
+ * Class to aid in resolving URLs by following redirects, which can optionally 
be performed
+ * asynchronously using a thread pool.
+ *
+ * @author William Farner
+ */
+public class UrlResolver {
+  private static final Logger LOG = 
Logger.getLogger(UrlResolver.class.getName());
+
+  private static final String TWITTER_UA = "Twitterbot/0.1";
+  private static final UrlResolverUtil URL_RESOLVER =
+      new UrlResolverUtil(Functions.constant(TWITTER_UA));
+
+  private static final ExceptionalFunction<String, String, IOException> 
RESOLVER =
+      new ExceptionalFunction<String, String, IOException>() {
+        @Override public String apply(String url) throws IOException {
+          return URL_RESOLVER.getEffectiveUrl(url, null);
+        }
+      };
+
+  private static ExceptionalFunction<String, String, IOException>
+      getUrlResolver(final @Nullable ProxyConfig proxyConfig) {
+    if (proxyConfig != null) {
+      return new ExceptionalFunction<String, String, IOException>() {
+        @Override public String apply(String url) throws IOException {
+          return URL_RESOLVER.getEffectiveUrl(url, proxyConfig);
+        }
+      };
+    } else {
+      return RESOLVER;
+    }
+  }
+
+  private final ExceptionalFunction<String, String, IOException> resolver;
+  private final int maxRedirects;
+
+  // Tracks the number of active tasks (threads in use).
+  private final Semaphore poolEntrySemaphore;
+  private final Integer threadPoolSize;
+
+  // Helps with signaling the handler.
+  private final Executor handlerExecutor;
+
+  // Manages the thread pool and task execution.
+  private ExecutorService executor;
+
+  // Cache to store resolved URLs.
+  private final Cache<String, String> urlCache = LRUCache.<String, 
String>builder()
+      .maxSize(10000)
+      .makeSynchronized(true)
+      .build();
+
+  // Variables to track connection/request stats.
+  private AtomicInteger requestCount = new AtomicInteger(0);
+  private AtomicInteger cacheHits = new AtomicInteger(0);
+  private AtomicInteger failureCount = new AtomicInteger(0);
+  // Tracks the time (in milliseconds) required to resolve URLs.
+  private final PrintableHistogram urlResolutionTimesMs = new 
PrintableHistogram(
+      1, 5, 10, 25, 50, 75, 100, 150, 200, 250, 300, 500, 750, 1000, 1500, 
2000);
+
+  private final Clock clock;
+  private final BackoffStrategy backoffStrategy;
+
+  @VisibleForTesting
+  UrlResolver(Clock clock, BackoffStrategy backoffStrategy,
+      ExceptionalFunction<String, String, IOException> resolver, int 
maxRedirects) {
+    this(clock, backoffStrategy, resolver, maxRedirects, null);
+  }
+
+  /**
+   * Creates a new asynchronous URL resolver.  A thread pool will be used to 
resolve URLs, and
+   * resolved URLs will be announced via {@code handler}.
+   *
+   * @param maxRedirects The maximum number of HTTP redirects to follow.
+   * @param threadPoolSize The number of threads to use for resolving URLs.
+   * @param proxyConfig The proxy settings with which to make the HTTP 
request, or null for the
+   *    default configured proxy.
+   */
+  public UrlResolver(int maxRedirects, int threadPoolSize, @Nullable 
ProxyConfig proxyConfig) {
+    this(Clock.SYSTEM_CLOCK,
+        new TruncatedBinaryBackoff(Amount.of(100L, Time.MILLISECONDS), 
Amount.of(1L, Time.SECONDS)),
+        getUrlResolver(proxyConfig), maxRedirects, threadPoolSize);
+  }
+
+  public UrlResolver(int maxRedirects, int threadPoolSize) {
+    this(maxRedirects, threadPoolSize, null);
+  }
+
+  private UrlResolver(Clock clock, BackoffStrategy backoffStrategy,
+      ExceptionalFunction<String, String, IOException> resolver, int 
maxRedirects,
+      @Nullable Integer threadPoolSize) {
+    this.clock = clock;
+    this.backoffStrategy = backoffStrategy;
+    this.resolver = resolver;
+    this.maxRedirects = maxRedirects;
+
+    if (threadPoolSize != null) {
+      this.threadPoolSize = threadPoolSize;
+      Preconditions.checkState(threadPoolSize > 0);
+      poolEntrySemaphore = new Semaphore(threadPoolSize);
+
+      // Start up the thread pool.
+      reset();
+
+      // Executor to send notifications back to the handler.  This also needs 
to be
+      // a daemon thread.
+      handlerExecutor =
+          Executors.newSingleThreadExecutor(new 
ThreadFactoryBuilder().setDaemon(true).build());
+    } else {
+      this.threadPoolSize = null;
+      poolEntrySemaphore = null;
+      handlerExecutor = null;
+    }
+  }
+
+  public Future<ResolvedUrl> resolveUrlAsync(final String url, final 
ResolvedUrlHandler handler) {
+    Preconditions.checkNotNull(
+        "Asynchronous URL resolution cannot be performed without a valid 
handler.", handler);
+
+    try {
+      poolEntrySemaphore.acquire();
+    } catch (InterruptedException e) {
+      LOG.log(Level.SEVERE, "Interrupted while waiting for thread to resolve 
URL: " + url, e);
+      return null;
+    }
+    final ListenableFutureTask<ResolvedUrl> future =
+        ListenableFutureTask.create(
+          new Callable<ResolvedUrl>() {
+            @Override public ResolvedUrl call() {
+              return resolveUrl(url);
+            }
+          });
+
+    future.addListener(new Runnable() {
+      @Override public void run() {
+        try {
+          handler.resolved(future);
+        } finally {
+          poolEntrySemaphore.release();
+        }
+      }
+    }, handlerExecutor);
+
+    executor.execute(future);
+    return future;
+  }
+
+  private void logThreadpoolInfo() {
+    LOG.info("Shutting down thread pool, available permits: "
+             + poolEntrySemaphore.availablePermits());
+    LOG.info("Queued threads? " + poolEntrySemaphore.hasQueuedThreads());
+    LOG.info("Queue length: " + poolEntrySemaphore.getQueueLength());
+  }
+
+  public void reset() {
+    Preconditions.checkState(threadPoolSize != null);
+    if (executor != null) {
+      Preconditions.checkState(executor.isShutdown(),
+          "The thread pool must be shut down before resetting.");
+      Preconditions.checkState(executor.isTerminated(), "There may still be 
pending async tasks.");
+    }
+
+    // Create a thread pool with daemon threads, so that they may be 
terminated when no
+    // application threads are running.
+    executor = Executors.newFixedThreadPool(threadPoolSize,
+        new 
ThreadFactoryBuilder().setDaemon(true).setNameFormat("UrlResolver[%d]").build());
+  }
+
+  /**
+   * Terminates the thread pool, waiting at most {@code waitSeconds} for 
active threads to complete.
+   * After this method is called, no more URLs may be submitted for resolution.
+   *
+   * @param waitSeconds The number of seconds to wait for active threads to 
complete.
+   */
+  public void clearAsyncTasks(int waitSeconds) {
+    Preconditions.checkState(threadPoolSize != null,
+        "finish() should not be called on a synchronous URL resolver.");
+
+    logThreadpoolInfo();
+    executor.shutdown(); // Disable new tasks from being submitted.
+    try {
+      // Wait a while for existing tasks to terminate
+      if (!executor.awaitTermination(waitSeconds, TimeUnit.SECONDS)) {
+        LOG.info("Pool did not terminate, forcing shutdown.");
+        logThreadpoolInfo();
+        List<Runnable> remaining = executor.shutdownNow();
+        LOG.info("Tasks still running: " + remaining);
+        // Wait a while for tasks to respond to being cancelled
+        if (!executor.awaitTermination(waitSeconds, TimeUnit.SECONDS)) {
+          LOG.warning("Pool did not terminate.");
+          logThreadpoolInfo();
+        }
+      }
+    } catch (InterruptedException e) {
+      LOG.log(Level.WARNING, "Interrupted while waiting for threadpool to 
finish.", e);
+      // (Re-)Cancel if current thread also interrupted
+      executor.shutdownNow();
+      // Preserve interrupt status
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  /**
+   * Resolves a URL synchronously.
+   *
+   * @param url The URL to resolve.
+   * @return The resolved URL.
+   */
+  public ResolvedUrl resolveUrl(String url) {
+    ResolvedUrl resolvedUrl = new ResolvedUrl();
+    resolvedUrl.setStartUrl(url);
+
+    String cached = urlCache.get(url);
+    if (cached != null) {
+      cacheHits.incrementAndGet();
+      resolvedUrl.setNextResolve(cached);
+      resolvedUrl.setEndState(EndState.CACHED);
+      return resolvedUrl;
+    }
+
+    String currentUrl = url;
+    long backoffMs = 0L;
+    String next = null;
+    for (int i = 0; i < maxRedirects; i++) {
+      try {
+        next = resolveOnce(currentUrl);
+
+        // If there was a 4xx or a 5xx, we''ll get a null back, so we pretend 
like we never advanced
+        // to allow for a retry within the redirect limit.
+        // TODO(John Sirois): we really need access to the return code here to 
do the right thing; ie:
+        // retry for internal server errors but probably not for unauthorized
+        if (next == null) {
+          if (i < maxRedirects - 1) { // don't wait if we're about to exit the 
loop
+            backoffMs = backoffStrategy.calculateBackoffMs(backoffMs);
+            try {
+              clock.waitFor(backoffMs);
+            } catch (InterruptedException e) {
+              Thread.currentThread().interrupt();
+              throw new RuntimeException(
+                  "Interrupted waiting to retry a failed resolution for: " + 
currentUrl, e);
+            }
+          }
+          continue;
+        }
+
+        backoffMs = 0L;
+        if (next.equals(currentUrl)) {
+          // We've reached the end of the redirect chain.
+          resolvedUrl.setEndState(EndState.REACHED_LANDING);
+          urlCache.put(url, currentUrl);
+          for (String intermediateUrl : resolvedUrl.getIntermediateUrls()) {
+            urlCache.put(intermediateUrl, currentUrl);
+          }
+          return resolvedUrl;
+        } else if (!url.equals(next)) {
+          resolvedUrl.setNextResolve(next);
+        }
+        currentUrl = next;
+      } catch (IOException e) {
+        LOG.log(Level.INFO, "Failed to resolve url: " + url, e);
+        resolvedUrl.setEndState(EndState.ERROR);
+        return resolvedUrl;
+      }
+    }
+
+    resolvedUrl.setEndState(next == null || url.equals(currentUrl) ? 
EndState.ERROR
+        : EndState.REDIRECT_LIMIT);
+    return resolvedUrl;
+  }
+
+  /**
+   * Resolves a url, following at most one redirect.  Thread-safe.
+   *
+   * @param url The URL to resolve.
+   * @return The result of following the URL through at most one redirect or 
null if the url could
+   *     not be followed
+   * @throws IOException If an error occurs while resolving the URL.
+   */
+  private String resolveOnce(String url) throws IOException {
+    requestCount.incrementAndGet();
+
+    String resolvedUrl = urlCache.get(url);
+    if (resolvedUrl != null) {
+      cacheHits.incrementAndGet();
+      return resolvedUrl;
+    }
+
+    try {
+      long startTimeMs = System.currentTimeMillis();
+      resolvedUrl = resolver.apply(url);
+      if (resolvedUrl == null) {
+        return null;
+      }
+
+      urlCache.put(url, resolvedUrl);
+
+      synchronized (urlResolutionTimesMs) {
+        urlResolutionTimesMs.addValue(System.currentTimeMillis() - 
startTimeMs);
+      }
+      return resolvedUrl;
+    } catch (IOException e) {
+      failureCount.incrementAndGet();
+      throw e;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return String.format("Cache: %s\nFailed requests: %d,\nResolution Times: 
%s",
+        urlCache, failureCount.get(),
+        urlResolutionTimesMs.toString());
+  }
+
+  /**
+   * Class to wrap the result of a URL resolution.
+   */
+  public static class ResolvedUrl {
+    public enum EndState {
+      REACHED_LANDING,
+      ERROR,
+      CACHED,
+      REDIRECT_LIMIT
+    }
+
+    private String startUrl;
+    private final List<String> resolveChain;
+    private EndState endState;
+
+    public ResolvedUrl() {
+      resolveChain = Lists.newArrayList();
+    }
+
+    @VisibleForTesting
+    public ResolvedUrl(EndState endState, String startUrl, String... 
resolveChain) {
+      this.endState = endState;
+      this.startUrl = startUrl;
+      this.resolveChain = Lists.newArrayList(resolveChain);
+    }
+
+    public String getStartUrl() {
+      return startUrl;
+    }
+
+    void setStartUrl(String startUrl) {
+      this.startUrl = startUrl;
+    }
+
+    /**
+     * Returns the last URL resolved following a redirect chain, or null if 
the startUrl is a
+     * landing URL.
+     */
+    public String getEndUrl() {
+      return resolveChain.isEmpty() ? null : Iterables.getLast(resolveChain);
+    }
+
+    void setNextResolve(String endUrl) {
+      this.resolveChain.add(endUrl);
+    }
+
+    /**
+     * Returns any immediate URLs encountered on the resolution chain.  If the 
startUrl redirects
+     * directly to the endUrl or they are the same the imtermediate URLs will 
be empty.
+     */
+    public Iterable<String> getIntermediateUrls() {
+      return resolveChain.size() <= 1 ? ImmutableList.<String>of()
+          : resolveChain.subList(0, resolveChain.size() - 1);
+    }
+
+    public EndState getEndState() {
+      return endState;
+    }
+
+    void setEndState(EndState endState) {
+      this.endState = endState;
+    }
+
+    public String toString() {
+      return String.format("%s -> %s [%s, %d redirects]",
+          startUrl, Joiner.on(" -> ").join(resolveChain), endState, 
resolveChain.size());
+    }
+  }
+
+  /**
+   * Interface to use for notifying the caller of resolved URLs.
+   */
+  public interface ResolvedUrlHandler {
+    /**
+     * Signals that a URL has been resolved to its target.  The implementation 
of this method must
+     * be thread safe.
+     *
+     * @param future The future that has finished resolving a URL.
+     */
+    public void resolved(Future<ResolvedUrl> future);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/UrlResolverUtil.java
----------------------------------------------------------------------
diff --git 
a/commons/src/main/java/org/apache/aurora/common/net/UrlResolverUtil.java 
b/commons/src/main/java/org/apache/aurora/common/net/UrlResolverUtil.java
new file mode 100644
index 0000000..4b95bb7
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/UrlResolverUtil.java
@@ -0,0 +1,148 @@
+/**
+ * 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 org.apache.aurora.common.net;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import org.apache.aurora.common.base.MorePreconditions;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.Proxy.Type;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Map;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * A utility that can resolve HTTP urls.
+ *
+ * @author John Sirois
+ */
+class UrlResolverUtil {
+
+  private static final Logger LOG = 
Logger.getLogger(UrlResolverUtil.class.getName());
+
+  // Default user-agent string to user for HTTP requests.
+  private static final String DEFAULT_USER_AGENT = "Lynxy/6.6.6dev.8 
libwww-FM/3.14159FM";
+
+  private static Map<String, String> checkNotBlank(Map<String, String> 
hostToUserAgent) {
+    Preconditions.checkNotNull(hostToUserAgent);
+    MorePreconditions.checkNotBlank(hostToUserAgent.entrySet());
+    return hostToUserAgent;
+  }
+
+  private final Function<? super URL, String> urlToUserAgent;
+
+  UrlResolverUtil(Map<String, String> hostToUserAgent) {
+    this(Functions.compose(Functions.forMap(checkNotBlank(hostToUserAgent), 
DEFAULT_USER_AGENT),
+        new Function<URL, String>() {
+          @Override public String apply(URL url) {
+            return url.getHost();
+          }
+        }));
+  }
+
+  UrlResolverUtil(Function<? super URL, String> urlToUserAgent) {
+    this.urlToUserAgent = Preconditions.checkNotNull(urlToUserAgent);
+  }
+
+  /**
+   * Returns the URL that {@code url} lands on, which will be the result of a 
3xx redirect,
+   * or {@code url} if the url does not redirect using an HTTP 3xx response 
code.  If there is a
+   * non-2xx or 3xx HTTP response code null is returned.
+   *
+   * @param url The URL to follow.
+   * @return The redirected URL, or {@code url} if {@code url} returns a 2XX 
response, otherwise
+   *         null
+   * @throws java.io.IOException If an error occurs while trying to follow the 
url.
+   */
+  String getEffectiveUrl(String url, @Nullable ProxyConfig proxyConfig) throws 
IOException {
+    Preconditions.checkNotNull(url);
+    // Don't follow https.
+    if (url.startsWith("https://";)) {
+      url = url.replace("https://";, "http://";);
+    } else if (!url.startsWith("http://";)) {
+      url = "http://"; + url;
+    }
+
+    URL urlObj = new URL(url);
+
+    HttpURLConnection con;
+    if (proxyConfig != null) {
+      Proxy proxy = new Proxy(Type.HTTP, proxyConfig.getProxyAddress());
+      con = (HttpURLConnection) urlObj.openConnection(proxy);
+      ProxyAuthorizer.adapt(proxyConfig).authorize(con);
+    } else {
+      con = (HttpURLConnection) urlObj.openConnection();
+    }
+    try {
+
+      // TODO(John Sirois): several commonly tweeted hosts 406 or 400 on HEADs 
and only work with GETs
+      // fix the call chain to be able to specify retry-with-GET
+      con.setRequestMethod("HEAD");
+
+      con.setUseCaches(true);
+      con.setConnectTimeout(5000);
+      con.setReadTimeout(5000);
+      con.setInstanceFollowRedirects(false);
+
+      // I hate to have to do this, but some URL shorteners don't respond 
otherwise.
+      con.setRequestProperty("User-Agent", urlToUserAgent.apply(urlObj));
+      try {
+        con.connect();
+      } catch (StringIndexOutOfBoundsException e) {
+        LOG.info("Got StringIndexOutOfBoundsException when fetching headers 
for " + url);
+        return null;
+      }
+
+      int responseCode = con.getResponseCode();
+      switch (responseCode / 100) {
+        case 2:
+          return url;
+        case 3:
+          String location = con.getHeaderField("Location");
+          if (location == null) {
+            if (responseCode != 304 /* not modified */) {
+              LOG.info(
+                  String.format("[%d] Location header was null for URL: %s", 
responseCode, url));
+            }
+            return url;
+          }
+
+          // HTTP 1.1 spec says this should be an absolute URI, but i see lots 
of instances where it
+          // is relative, so we need to check.
+          try {
+            String domain = UrlHelper.getDomainChecked(location);
+            if (domain == null || domain.isEmpty()) {
+              // This is a relative URI.
+              location = "http://"; + UrlHelper.getDomain(url) + location;
+            }
+          } catch (URISyntaxException e) {
+            LOG.info("location contained an invalid URI: " + location);
+          }
+
+          return location;
+        default:
+          LOG.info("Failed to resolve url: " + url + " with: "
+                   + responseCode + " -> " + con.getResponseMessage());
+          return null;
+      }
+    } finally {
+      con.disconnect();
+    }
+  }
+}

Reply via email to