| // Copyright 2013 Google Inc. All Rights Reserved. |
| // |
| // 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.enterprise.adaptor.fs; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.enterprise.adaptor.DocId; |
| import com.google.enterprise.adaptor.DocIdEncoder; |
| |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.Writer; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.text.MessageFormat; |
| import java.util.Locale; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| class HtmlResponseWriter implements Closeable { |
| private static final Logger log |
| = Logger.getLogger(HtmlResponseWriter.class.getName()); |
| |
| private enum State { |
| /** Initial state after construction. */ |
| INITIAL, |
| /** |
| * {@link #start} was just called, so the HTML header is in place, but no |
| * other content. |
| */ |
| STARTED, |
| /** {@link #finish} has been called, so the HTML footer has been written. */ |
| FINISHED, |
| /** The writer has been closed. */ |
| CLOSED, |
| } |
| |
| private final Writer writer; |
| private final DocIdEncoder docIdEncoder; |
| private final Locale locale; |
| private DocId docId; |
| private URI docUri; |
| private State state = State.INITIAL; |
| |
| public HtmlResponseWriter(Writer writer, DocIdEncoder docIdEncoder, |
| Locale locale) { |
| if (writer == null) { |
| throw new NullPointerException(); |
| } |
| if (docIdEncoder == null) { |
| throw new NullPointerException(); |
| } |
| if (locale == null) { |
| throw new NullPointerException(); |
| } |
| this.writer = writer; |
| this.docIdEncoder = docIdEncoder; |
| this.locale = locale; |
| } |
| |
| /** |
| * Start writing HTML document. |
| * |
| * @param docId the DocId for the document being written out |
| * @param label possibly-{@code null} title or name of {@code docId} |
| */ |
| public void start(DocId docId, String label) |
| throws IOException { |
| if (state != State.INITIAL) { |
| throw new IllegalStateException("In unexpected state: " + state); |
| } |
| this.docId = docId; |
| this.docUri = docIdEncoder.encodeDocId(docId); |
| // TODO(ejona): Localize. |
| String header = MessageFormat.format("{0} {1}", |
| "Folder", computeLabel(label, docId)); |
| writer.write("<!DOCTYPE html>\n<html><head><title>"); |
| writer.write(escapeContent(header)); |
| writer.write("</title></head><body><h1>"); |
| writer.write(escapeContent(header)); |
| writer.write("</h1>"); |
| state = State.STARTED; |
| } |
| |
| /** |
| * @param docId docId to add as a link in the document |
| * @param label possibly-{@code null} title or description of {@code docId} |
| */ |
| public void addLink(DocId doc, String label) throws IOException { |
| if (state != State.STARTED) { |
| throw new IllegalStateException("In unexpected state: " + state); |
| } |
| if (doc == null) { |
| throw new NullPointerException(); |
| } |
| writer.write("<li><a href=\""); |
| writer.write(escapeAttributeValue(encodeDocId(doc))); |
| writer.write("\">"); |
| writer.write(escapeContent(computeLabel(label, doc))); |
| writer.write("</a></li>"); |
| } |
| |
| /** |
| * Complete HTML body and flush. |
| */ |
| public void finish() throws IOException { |
| log.entering("HtmlResponseWriter", "finish"); |
| if (state != State.STARTED) { |
| throw new IllegalStateException("In unexpected state: " + state); |
| } |
| writer.write("</body></html>"); |
| writer.flush(); |
| state = State.FINISHED; |
| log.exiting("HtmlResponseWriter", "finish"); |
| } |
| |
| /** |
| * Close underlying writer. You will generally want to call {@link #finish} |
| * first. |
| */ |
| @Override |
| public void close() throws IOException { |
| log.entering("HtmlResponseWriter", "close"); |
| writer.close(); |
| state = State.CLOSED; |
| log.exiting("HtmlResponseWriter", "close"); |
| } |
| |
| /** |
| * Encodes a DocId into a URI formatted as a string. |
| */ |
| private String encodeDocId(DocId doc) { |
| log.entering("HtmlResponseWriter", "encodeDocId", doc); |
| URI uri = docIdEncoder.encodeDocId(doc); |
| uri = relativize(docUri, uri); |
| String encoded = uri.toASCIIString(); |
| log.exiting("HtmlResponseWriter", "encodeDocId", encoded); |
| return encoded; |
| } |
| |
| /** |
| * Produce a relative URI from {@code uri} relative to {@code base}, assuming |
| * both URIs are hierarchial. If possible, a relative URI will be returned |
| * that can be resolved from {@code base}, otherwise {@code uri} will be |
| * returned. |
| * |
| * <p>Necessary since {@link URI#relativize} is broken when considering |
| * http://host/path vs http://host/path/ as the base URI. See |
| * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6226081"> |
| * Bug 6226081</a> for more information. In addition, this version uses |
| * {@code ..} when possible unlike {@link URI#relativize}. |
| */ |
| @VisibleForTesting |
| static URI relativize(URI base, URI uri) { |
| if (base.getScheme() == null || !base.getScheme().equals(uri.getScheme()) |
| || base.getAuthority() == null |
| || !base.getAuthority().equals(uri.getAuthority())) { |
| return uri; |
| } |
| if (base.equals(uri)) { |
| return URI.create("#"); |
| } |
| // These paths are known to start with a / or be the empty string; since the |
| // URIs have a scheme, we know they are absolute. |
| String basePath = base.getPath(); |
| String uriPath = uri.getPath(); |
| |
| String[] basePathParts = basePath.split("/", -1); |
| String[] uriPathParts = uriPath.split("/", -1); |
| int i = 0; |
| // Remove common folders. Since we are looking at folders, we don't compare |
| // the last elements in the array, because they were after the last '/' in |
| // the URIs. |
| for (; i < basePathParts.length - 1 && i < uriPathParts.length - 1; i++) { |
| if (!basePathParts[i].equals(uriPathParts[i])) { |
| break; |
| } |
| } |
| StringBuilder pathBuilder = new StringBuilder(); |
| for (int j = i; j < basePathParts.length - 1; j++) { |
| pathBuilder.append("../"); |
| } |
| for (; i < uriPathParts.length; i++) { |
| pathBuilder.append(uriPathParts[i]); |
| pathBuilder.append("/"); |
| } |
| String path = pathBuilder.substring(0, pathBuilder.length() - 1); |
| int colonLocation = path.indexOf(":"); |
| int slashLocation = path.indexOf("/"); |
| if (colonLocation != -1 |
| && (slashLocation == -1 || colonLocation < slashLocation)) { |
| // If there is a colon before the first slash, then it is easy to confuse |
| // this relative URI for an absolute URI. Thus, we prepend a ./ so that |
| // the beginning is obviously not a scheme. |
| path = "./" + path; |
| } |
| try { |
| return new URI(null, null, path, uri.getQuery(), uri.getFragment()); |
| } catch (URISyntaxException ex) { |
| throw new AssertionError(ex); |
| } |
| } |
| |
| private String computeLabel(String label, DocId doc) { |
| if (Strings.isNullOrEmpty(label)) { |
| // Use the last part of the URL if an item doesn't have a title. The last |
| // part of the URL will generally be a filename in this case. |
| String[] parts = doc.getUniqueId().split("/", 0); |
| label = parts[parts.length - 1]; |
| } |
| return label; |
| } |
| |
| private String escapeContent(String raw) { |
| return raw.replace("&", "&").replace("<", "<"); |
| } |
| |
| private String escapeAttributeValue(String raw) { |
| return escapeContent(raw).replace("\"", """).replace("'", "'"); |
| } |
| } |