PathURLConnection.java

/*
 * Copyright 2015 Google Inc.
 *
 * 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 com.google.common.jimfs;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Ascii;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * {@code URLConnection} implementation.
 *
 * @author Colin Decker
 */
final class PathURLConnection extends URLConnection {

  /*
   * This implementation should be able to work for any proper file system implementation... it
   * might be useful to release it and make it usable by other file systems.
   */

  private static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss \'GMT\'";
  private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";

  private InputStream stream;
  private ImmutableListMultimap<String, String> headers = ImmutableListMultimap.of();

  PathURLConnection(URL url) {
    super(checkNotNull(url));
  }

  @Override
  public void connect() throws IOException {
    if (stream != null) {
      return;
    }

    Path path = Paths.get(toUri(url));

    long length;
    if (Files.isDirectory(path)) {
      // Match File URL behavior for directories by having the stream contain the filenames in
      // the directory separated by newlines.
      StringBuilder builder = new StringBuilder();
      try (DirectoryStream<Path> files = Files.newDirectoryStream(path)) {
        for (Path file : files) {
          builder.append(file.getFileName()).append('\n');
        }
      }
      byte[] bytes = builder.toString().getBytes(UTF_8);
      stream = new ByteArrayInputStream(bytes);
      length = bytes.length;
    } else {
      stream = Files.newInputStream(path);
      length = Files.size(path);
    }

    FileTime lastModified = Files.getLastModifiedTime(path);
    String contentType =
        MoreObjects.firstNonNull(Files.probeContentType(path), DEFAULT_CONTENT_TYPE);

    ImmutableListMultimap.Builder<String, String> builder = ImmutableListMultimap.builder();
    builder.put("content-length", "" + length);
    builder.put("content-type", contentType);
    if (lastModified != null) {
      DateFormat format = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US);
      format.setTimeZone(TimeZone.getTimeZone("GMT"));
      builder.put("last-modified", format.format(new Date(lastModified.toMillis())));
    }

    headers = builder.build();
  }

  private static URI toUri(URL url) throws IOException {
    try {
      return url.toURI();
    } catch (URISyntaxException e) {
      throw new IOException("URL " + url + " cannot be converted to a URI", e);
    }
  }

  @Override
  public InputStream getInputStream() throws IOException {
    connect();
    return stream;
  }

  @SuppressWarnings("unchecked") // safe by specification of ListMultimap.asMap()
  @Override
  public Map<String, List<String>> getHeaderFields() {
    try {
      connect();
    } catch (IOException e) {
      return ImmutableMap.of();
    }
    return (ImmutableMap<String, List<String>>) (ImmutableMap<String, ?>) headers.asMap();
  }

  @Override
  public @Nullable String getHeaderField(String name) {
    try {
      connect();
    } catch (IOException e) {
      return null;
    }

    // no header should have more than one value
    return Iterables.getFirst(headers.get(Ascii.toLowerCase(name)), null);
  }
}