/* ***** BEGIN LICENSE BLOCK ***** * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is com.jkristian.protocol.p4. * * The Initial Developer of the Original Code is John M. Kristian. * Portions created by the Initial Developer are Copyright (C) 2005 * the Initial Developer. All Rights Reserved. * * Contributor(s): * John M. Kristian <jk2005@engineer.com> * * ***** END LICENSE BLOCK ***** */ package com.jkristian.protocol.p4; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.net.URLStreamHandler; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import com.jkristian.io.ByteCopier; /** * The handler for the URI scheme "p4". This class enables URL-based * applications to read files from a Perforce depot or workspace. To install it, * run the JVM with -Djava.protocol.handler.pkgs=com.jkristian.protocol, or call * Handler.install. For a fuller explanation, see * http://java.sun.com/developer/onlineTraining/protocolhandlers/ * <p> * Supported URLs have the form * <code>p4://user:password@]host:port//depot/file%23version</code> where: * <ul> * <li>user corresponds to P4USER.</li> * <li>password corresponds to P4PASSWD.</li> * <li>host:port corresponds to P4PORT.</li> * <li>//depot/file#version is a Perforce <a * href="http://www.perforce.com/perforce/doc.051/manuals/cmdref/o.fspecs.html">file * specification </a> with an optional revision specifier. A '#' must be encoded * as %23 (to prevent its interpretation as a fragment reference).</li> * </ul> * <p> * This implementation requires the Perforce command line program `p4` to be * available. (Most of the work is done by executing `p4 print`.) * <p> * Writing is not supported; that is, you can't send data via a p4: URL. (I * imagine a future implementation might check out workspace files for editing * and/or submit new versions to the depot.) The URL scheme "p4" is not * registered with IANA (yet). * * @author John Kristian */ public class Handler extends URLStreamHandler { /** Make this Handler available to the default URLStreamHandlerFactory. */ public static void install() { String newValue = Handler.class.getPackage().getName(); newValue = newValue.substring(0, newValue.lastIndexOf('.')); String oldValue = System.getProperty(PKGS); if (oldValue != null) { newValue += ("|" + oldValue); } System.setProperty(PKGS, newValue); } private static final String PKGS = "java.protocol.handler.pkgs"; /** Copy headers and data from the given URLs to System.out. */ public static void main(String[] URLs) throws Exception { install(); for (int u = 0; u < URLs.length; ++u) { URL url = new URL(URLs[u]); URLConnection connection = url.openConnection(); for (int h = 0; true; ++h) { String name = connection.getHeaderFieldKey(h); String value = connection.getHeaderField(h); if (value == null) break; if (name == null) System.out.println(value); else System.out.println(name + ": " + value); } System.out.println(); InputStream data = connection.getInputStream(); try { ByteCopier.copyAll(data, System.out); } finally { data.close(); } System.out.println(); System.out.println("-------------------"); } } public static final int DEFAULT_PORT = 1666; protected int getDefaultPort() { return DEFAULT_PORT; } protected URLConnection openConnection(URL url) throws IOException { return new P4URLConnection(url); } private static class P4URLConnection extends URLConnection { public P4URLConnection(URL url) { super(url); } private static final String[] STRING_ARRAY = new String[0]; protected String header0 = ""; protected Map headerFields = new HashMap(); protected InputStream inputStream = null; public Map getHeaderFields() { connectIfNecessary(); return headerFields; } public String getHeaderFieldKey(int n) { connectIfNecessary(); if (n == 0) return null; Map.Entry entry = getHeaderFieldEntry(n); return (entry == null) ? null : (String) entry.getKey(); } public String getHeaderField(int n) { connectIfNecessary(); if (n == 0) return header0; Map.Entry entry = getHeaderFieldEntry(n); return (entry == null) ? null : (String) entry.getValue(); } protected Map.Entry getHeaderFieldEntry(int n) { if (n > 0) { for (Iterator iter = headerFields.entrySet().iterator(); iter.hasNext(); --n) { Object entry = iter.next(); if (n == 1) return (Map.Entry) entry; } } return null; } protected void connectIfNecessary() { if (!connected) { try { connect(); } catch (IOException ignored) { } } } public InputStream getInputStream() throws IOException { if (!connected) connect(); return inputStream; } private static final String UTF8 = "UTF-8"; public void connect() throws IOException { String path = URLDecoder.decode(url.getPath(), UTF8); if (File.separatorChar == '\\' && path.matches("/[a-zA-Z]:.*")) { // Windows path = path.substring(1); } List command = new ArrayList(3); command.add("p4"); addGlobalOptions(command); command.add("print"); command.add(path); Runtime runtime = Runtime.getRuntime(); Process source = runtime.exec((String[]) command.toArray(STRING_ARRAY)); InputStream dataStream = source.getInputStream(); // The expected dataStream is a header line followed by the file // contents or, if something goes wrong, no data at all. { // Collect error data in a buffer: ByteArrayOutputStream errorBuffer = new ByteArrayOutputStream(); SteerableOutputStream errorTarget = new SteerableOutputStream(errorBuffer); Thread errorReader = new Thread( new ByteCopier(source.getErrorStream(), errorTarget)); errorReader.start(); ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(); while (true) { int b = dataStream.read(); if (b < 0) // end of file break; headerBuffer.write(b); if (b == '\n') // end of line break; } headerBuffer.close(); if (headerBuffer.size() > 0) { header0 = trimLine(headerBuffer.toByteArray()); // Redirect the error stream to System.err: synchronized (errorTarget) { // pause the errorReader errorTarget.getOutput().close(); System.err.write(errorBuffer.toByteArray()); errorTarget.setOutput(System.err); } } else { // No header, no data. // Collect all the error messages: while (true) { try { errorReader.join(); break; } catch (InterruptedException ignored) { // and try again. } } errorTarget.close(); // Propagate an exception containing the error messages: String message = trimLine(errorBuffer.toByteArray()); if (message == null || message.length() <= 0) { message = path; } throw new FileNotFoundException(message); } } // TODO: inputStream = new TeeInputStream(header0, dataStream); inputStream = dataStream; connected = true; } private void addGlobalOptions(List command) throws UnsupportedEncodingException { String user = url.getUserInfo(); if (user != null) { int colon = user.indexOf(':'); if (colon >= 0) { command.add("-P"); command.add(URLDecoder.decode(user.substring(colon + 1), UTF8)); user = (colon == 0) ? null : user.substring(0, colon); } else if (user.length() <= 0) { user = null; } } if (user != null) { command.add("-u"); command.add(URLDecoder.decode(user, UTF8)); } String host = url.getHost(); if (host != null && host.length() > 0) { command.add("-p"); int port = url.getPort(); if (port <= 0) port = DEFAULT_PORT; command.add(host + ":" + port); } } private static final String trimLine(byte[] lineBytes) throws UnsupportedEncodingException { int length = lineBytes.length; if (length > 0 && lineBytes[length - 1] == '\n') --length; if (length > 0 && lineBytes[length - 1] == '\r') --length; return new String(lineBytes, 0, length, "ISO-8859-1"); } protected class SteerableOutputStream extends FilterOutputStream { public SteerableOutputStream(OutputStream out) { super(out); } public void setOutput(OutputStream out) { this.out = out; } public OutputStream getOutput() { return this.out; } public synchronized void write(int datum) throws IOException { super.write(datum); } public synchronized void write(byte[] data) throws IOException { super.write(data); } public synchronized void write(byte[] data, int offset, int length) throws IOException { super.write(data, offset, length); } public synchronized void flush() throws IOException { super.flush(); } public synchronized void close() throws IOException { super.close(); } } // TODO: Cache copies of files that have been read: // //static Map header2data = new WeakHashMap(); // //public static final class TeeInputStream extends FilterInputStream //{ // public TeeInputStream(String header, InputStream in) ... // header2data.put(header, a copy of the data) //} } }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#12 | 5023 | John Kristian | upgraded to io-v0_3 | ||
#11 | 5022 | John Kristian | beefed up newline handling | ||
#10 | 5021 | John Kristian | cross-reference the I/O packages (from SourceForge) | ||
#9 | 5020 | John Kristian | method renamed | ||
#8 | 5018 | John Kristian |
Refined getContentType algorithm. Don't ignore IOException. |
||
#7 | 5017 | John Kristian | add a charset only if p4Type.endsWith("unicode") | ||
#6 | 5016 | John Kristian | added Content-Type header | ||
#5 | 5015 | John Kristian | commentary | ||
#4 | 5014 | John Kristian | refined | ||
#3 | 5008 | John Kristian | Don't normalize //depot to /depot. | ||
#2 | 5007 | John Kristian |
Handle relative URIs (e.g. ../package.dtd). |
||
#1 | 5006 | John Kristian | Enable URL-based Java applications to get files from a Perforce depot. |