001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.vfs;
031:
032: import com.caucho.util.Alarm;
033: import com.caucho.util.CharBuffer;
034: import com.caucho.util.L10N;
035: import com.caucho.util.LruCache;
036: import com.caucho.util.QDate;
037:
038: import org.xml.sax.Attributes;
039: import org.xml.sax.SAXException;
040:
041: import java.io.IOException;
042: import java.util.ArrayList;
043: import java.util.Map;
044:
045: /**
046: * The HTTP scheme. Currently it supports GET and POST.
047: *
048: * <p>TODO: support WEBDAV, enabling the full Path API.
049: */
050: public class HttpPath extends FilesystemPath {
051: protected static L10N L = new L10N(HttpPath.class);
052:
053: protected static LruCache<String, CacheEntry> _cache = new LruCache<String, CacheEntry>(
054: 1024);
055:
056: protected String _host;
057: protected int _port;
058: protected String _query;
059:
060: protected String _virtualHost;
061:
062: protected CacheEntry _cacheEntry;
063:
064: /**
065: * Creates a new HTTP root path with a host and a port.
066: *
067: * @param host the target host
068: * @param port the target port, if zero, uses port 80.
069: */
070: public HttpPath(String host, int port) {
071: super (null, "/", "/");
072:
073: _root = this ;
074: _host = host;
075: _port = port == 0 ? 80 : port;
076: }
077:
078: /**
079: * Creates a new HTTP sub path.
080: *
081: * @param root the HTTP filesystem root
082: * @param userPath the argument to the calling lookup()
083: * @param newAttributes any attributes passed to http
084: * @param path the full normalized path
085: * @param query any query string
086: */
087: HttpPath(FilesystemPath root, String userPath,
088: Map<String, Object> newAttributes, String path, String query) {
089: super (root, userPath, path);
090:
091: _host = ((HttpPath) root)._host;
092: _port = ((HttpPath) root)._port;
093: _query = query;
094:
095: if (newAttributes != null) {
096: _virtualHost = (String) newAttributes.get("host");
097: }
098: }
099:
100: /**
101: * Overrides the default lookup to parse the host and port
102: * before parsing the path.
103: *
104: * @param userPath the path passed in by the user
105: * @param newAttributes attributes passed by the user
106: *
107: * @return the final path.
108: */
109: public Path lookupImpl(String userPath,
110: Map<String, Object> newAttributes) {
111: String newPath;
112:
113: if (userPath == null)
114: return _root.fsWalk(getPath(), newAttributes, "/");
115:
116: int length = userPath.length();
117: int colon = userPath.indexOf(':');
118: int slash = userPath.indexOf('/');
119:
120: // parent handles scheme:xxx
121: if (colon != -1 && (colon < slash || slash == -1))
122: return super .lookupImpl(userPath, newAttributes);
123:
124: // //hostname
125: if (slash == 0 && length > 1 && userPath.charAt(1) == '/')
126: return schemeWalk(userPath, newAttributes, userPath, 0);
127:
128: // /path
129: else if (slash == 0)
130: newPath = normalizePath("/", userPath, 0, '/');
131:
132: // path
133: else
134: newPath = normalizePath(_pathname, userPath, 0, '/');
135:
136: // XXX: does missing root here cause problems with restrictions?
137: return _root.fsWalk(userPath, newAttributes, newPath);
138: }
139:
140: /**
141: * Walk down the path starting from the portion immediately following
142: * the scheme. i.e. schemeWalk is responsible for parsing the host and
143: * port from the URL.
144: *
145: * @param userPath the user's passed in path
146: * @param attributes the attributes for the new path
147: * @param uri the normalized full uri
148: * @param offset offset into the uri to start processing, i.e. after the
149: * scheme.
150: *
151: * @return the looked-up path.
152: */
153: protected Path schemeWalk(String userPath,
154: Map<String, Object> attributes, String uri, int offset) {
155: int length = uri.length();
156:
157: if (length < 2 + offset || uri.charAt(offset) != '/'
158: || uri.charAt(offset + 1) != '/')
159: throw new RuntimeException(L.l("bad scheme in `{0}'", uri));
160:
161: CharBuffer buf = CharBuffer.allocate();
162: int i = 2 + offset;
163: int ch = 0;
164: for (; i < length && (ch = uri.charAt(i)) != ':' && ch != '/'
165: && ch != '?'; i++) {
166: buf.append((char) ch);
167: }
168:
169: String host = buf.close();
170: if (host.length() == 0)
171: throw new RuntimeException(L.l("bad host in `{0}'", uri));
172:
173: int port = 0;
174: if (ch == ':') {
175: for (i++; i < length && (ch = uri.charAt(i)) >= '0'
176: && ch <= '9'; i++) {
177: port = 10 * port + uri.charAt(i) - '0';
178: }
179: }
180:
181: if (port == 0)
182: port = 80;
183:
184: HttpPath root = create(host, port);
185:
186: return root.fsWalk(userPath, attributes, uri.substring(i));
187: }
188:
189: /**
190: * Scans the path portion of the URI, i.e. everything after the
191: * host and port.
192: *
193: * @param userPath the user's supplied path
194: * @param attributes the attributes for the new path
195: * @param uri the full uri for the new path.
196: *
197: * @return the found path.
198: */
199: public Path fsWalk(String userPath, Map<String, Object> attributes,
200: String uri) {
201: String path;
202: String query = null;
203: int queryIndex = uri.indexOf('?');
204: if (queryIndex >= 0) {
205: path = uri.substring(0, queryIndex);
206: query = uri.substring(queryIndex + 1);
207: } else
208: path = uri;
209:
210: if (path.length() == 0)
211: path = "/";
212:
213: return create(_root, userPath, attributes, path, query);
214: }
215:
216: protected HttpPath create(String host, int port) {
217: return new HttpPath(host, port);
218: }
219:
220: protected HttpPath create(FilesystemPath root, String userPath,
221: Map<String, Object> newAttributes, String path, String query) {
222: return new HttpPath(root, userPath, newAttributes, path, query);
223: }
224:
225: /**
226: * Returns the scheme, http.
227: */
228: public String getScheme() {
229: return "http";
230: }
231:
232: /**
233: * Returns a full URL for the path.
234: */
235: public String getURL() {
236: int port = getPort();
237:
238: return (getScheme() + "://" + getHost()
239: + (port == 80 ? "" : ":" + getPort()) + getPath() + (_query == null ? ""
240: : "?" + _query));
241: }
242:
243: /**
244: * Returns the host part of the url.
245: */
246: public String getHost() {
247: return _host;
248: }
249:
250: /**
251: * Returns the port part of the url.
252: */
253: public int getPort() {
254: return _port;
255: }
256:
257: /**
258: * Returns the user's path.
259: */
260: public String getUserPath() {
261: return _userPath;
262: }
263:
264: /**
265: * Returns the virtual host, if any.
266: */
267: public String getVirtualHost() {
268: return _virtualHost;
269: }
270:
271: /**
272: * Returns the query string.
273: */
274: public String getQuery() {
275: return _query;
276: }
277:
278: /**
279: * Returns the last modified time.
280: */
281: public long getLastModified() {
282: return getCache().lastModified;
283: }
284:
285: /**
286: * Returns the file's length
287: */
288: public long getLength() {
289: return getCache().length;
290: }
291:
292: /**
293: * Returns true if the file exists.
294: */
295: public boolean exists() {
296: return getCache().lastModified >= 0;
297: }
298:
299: /**
300: * Returns true if the file exists.
301: */
302: public boolean isFile() {
303: return !getPath().endsWith("/") && getCache().lastModified >= 0;
304: }
305:
306: /**
307: * Returns true if the file is readable.
308: */
309: public boolean canRead() {
310: return isFile();
311: }
312:
313: /**
314: * Returns the last modified time.
315: */
316: public boolean isDirectory() {
317: return getPath().endsWith("/") && getCache().lastModified >= 0;
318: }
319:
320: /**
321: * @return The contents of this directory or null if the path does not
322: * refer to a directory.
323: */
324: /*
325: public String []list() throws IOException
326: {
327: try {
328: HttpStream stream = (HttpStream) openReadWriteImpl();
329: stream.setMethod("PROPFIND");
330: stream.setAttribute("Depth", "1");
331:
332: WriteStream os = new WriteStream(stream);
333: os.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
334: os.println("<propfind xmlns=\"DAV:\"><prop>");
335: os.println("<resourcetype/>");
336: os.println("</prop></propfind>");
337: os.flush();
338:
339: ReadStream is = new ReadStream(stream);
340:
341: ListHandler handler = new ListHandler(getPath());
342: XmlParser parser = new XmlParser();
343: parser.setContentHandler(handler);
344:
345: parser.parse(is);
346:
347: is.close();
348: os.close();
349: stream.close();
350:
351: ArrayList<String> names = handler.getNames();
352: String []list = new String[names.size()];
353: names.toArray(list);
354:
355: return list;
356: } catch (Exception e) {
357: throw new IOException(L.l("list() is not supported by this server"));
358: }
359: }
360: */
361:
362: protected CacheEntry getCache() {
363: if (_cacheEntry == null) {
364: synchronized (_cache) {
365: _cacheEntry = _cache.get(getPath());
366: if (_cacheEntry == null) {
367: _cacheEntry = new CacheEntry();
368: _cache.put(getPath(), _cacheEntry);
369: }
370: }
371: }
372:
373: long now = Alarm.getCurrentTime();
374: synchronized (_cacheEntry) {
375: try {
376: if (_cacheEntry.expires > now)
377: return _cacheEntry;
378:
379: HttpStreamWrapper stream = (HttpStreamWrapper) openReadImpl();
380: stream.setHead(true);
381: stream.setSocketTimeout(120000);
382:
383: String status = (String) stream.getAttribute("status");
384: if (status.equals("200")) {
385: String lastModified = (String) stream
386: .getAttribute("last-modified");
387:
388: _cacheEntry.lastModified = 0;
389: if (lastModified != null) {
390: QDate date = QDate.getGlobalDate();
391: synchronized (date) {
392: _cacheEntry.lastModified = date
393: .parseDate(lastModified);
394: }
395: }
396:
397: String length = (String) stream
398: .getAttribute("content-length");
399: _cacheEntry.length = 0;
400: if (length != null) {
401: _cacheEntry.length = Integer.parseInt(length);
402: }
403: } else
404: _cacheEntry.lastModified = -1;
405:
406: _cacheEntry.expires = now + 5000;
407:
408: stream.close();
409: return _cacheEntry;
410: } catch (Exception e) {
411: _cacheEntry.lastModified = -1;
412: _cacheEntry.expires = now + 5000;
413:
414: return _cacheEntry;
415: }
416: }
417: }
418:
419: /**
420: * Returns a read stream for a GET request.
421: */
422: public StreamImpl openReadImpl() throws IOException {
423: return HttpStream.openRead(this );
424: }
425:
426: /**
427: * Returns a read/write pair for a POST request.
428: */
429: public StreamImpl openReadWriteImpl() throws IOException {
430: return HttpStream.openReadWrite(this );
431: }
432:
433: @Override
434: protected Path cacheCopy() {
435: return new HttpPath(getRoot(), getUserPath(), null, getPath(),
436: _query);
437: }
438:
439: /**
440: * Returns the string form of the http path.
441: */
442: public String toString() {
443: return getURL();
444: }
445:
446: /**
447: * Returns a hashCode for the path.
448: */
449: public int hashCode() {
450: return 65537 * super .hashCode() + 37 * _host.hashCode() + _port;
451: }
452:
453: /**
454: * Overrides equals to test for equality with an HTTP path.
455: */
456: public boolean equals(Object o) {
457: if (!(o instanceof HttpPath))
458: return false;
459:
460: HttpPath test = (HttpPath) o;
461:
462: if (!_host.equals(test._host))
463: return false;
464: else if (_port != test._port)
465: return false;
466: else if (_query != null && !_query.equals(test._query))
467: return false;
468: else if (_query == null && test._query != null)
469: return false;
470: else
471: return true;
472: }
473:
474: static class CacheEntry {
475: long lastModified;
476: long length;
477: boolean canRead;
478: long expires;
479: }
480:
481: static class ListHandler extends org.xml.sax.helpers.DefaultHandler {
482: String _prefix;
483: ArrayList<String> _names = new ArrayList<String>();
484: boolean _inHref;
485:
486: ListHandler(String prefix) {
487: _prefix = prefix;
488: }
489:
490: ArrayList<String> getNames() {
491: return _names;
492: }
493:
494: public void startElement(String uri, String localName,
495: String qName, Attributes attributes) {
496: if (localName.equals("href"))
497: _inHref = true;
498: }
499:
500: public void characters(char[] data, int offset, int length)
501: throws SAXException {
502: if (!_inHref)
503: return;
504:
505: String href = new String(data, offset, length).trim();
506: if (!href.startsWith(_prefix))
507: return;
508:
509: href = href.substring(_prefix.length());
510: if (href.startsWith("/"))
511: href = href.substring(1);
512:
513: int p = href.indexOf('/');
514: if (href.equals("") || p == 0)
515: return;
516:
517: if (p < 0)
518: _names.add(href);
519: else
520: _names.add(href.substring(0, p));
521: }
522:
523: public void endElement(String uri, String localName,
524: String qName) throws SAXException {
525: if (localName.equals("href"))
526: _inHref = false;
527: }
528: }
529: }
|