com.google.enterprise.adaptor.sharepoint.HtmlResponseWriter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.enterprise.adaptor.sharepoint.HtmlResponseWriter.java

Source

// Copyright 2012 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.sharepoint;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Multimap;
import com.google.common.io.CountingOutputStream;
import com.google.enterprise.adaptor.DocId;
import com.google.enterprise.adaptor.DocIdEncoder;
import com.google.enterprise.adaptor.DocIdPusher;

import com.microsoft.schemas.sharepoint.soap.ObjectType;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;

class HtmlResponseWriter implements Closeable {
    /**
     * The number of bytes that may be buffered within the streams. It should
     * error on the side of being too large.
     */
    private static final long POSSIBLY_BUFFERED_BYTES = 1024;

    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 #startSection} has been called, so we are within a section. */
        IN_SECTION,
        /** {@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 final long thresholdBytes;
    private final CountingOutputStream countingOutputStream;
    private final DocIdPusher pusher;
    private final Executor executor;
    private DocId docId;
    private URI docUri;
    private State state = State.INITIAL;
    private final Collection<DocId> overflowDocIds = new ArrayList<DocId>(1024);

    public HtmlResponseWriter(OutputStream os, Charset charset, DocIdEncoder docIdEncoder, Locale locale,
            long thresholdBytes, DocIdPusher pusher, Executor executor) {
        if (os == null) {
            throw new NullPointerException();
        }
        if (charset == null) {
            throw new NullPointerException();
        }
        if (docIdEncoder == null) {
            throw new NullPointerException();
        }
        if (locale == null) {
            throw new NullPointerException();
        }
        if (pusher == null) {
            throw new NullPointerException();
        }
        if (executor == null) {
            throw new NullPointerException();
        }
        countingOutputStream = new CountingOutputStream(os);
        this.writer = new OutputStreamWriter(countingOutputStream, charset);
        this.docIdEncoder = docIdEncoder;
        this.locale = locale;
        this.thresholdBytes = thresholdBytes;
        this.pusher = pusher;
        this.executor = executor;
    }

    /**
     * Start writing HTML document.
     *
     * @param docId the DocId for the document being written out
     * @param type type of document referred to by {@code docId}
     * @param label possibly-{@code null} title or name of {@code docId}
     */
    public void start(DocId docId, ObjectType type, String label) throws IOException {
        if (state != State.INITIAL) {
            throw new IllegalStateException("In unexpected state: " + state);
        }
        this.docId = docId;
        this.docUri = docIdEncoder.encodeDocId(docId);
        String documentLabel = computeLabel(label, docId);
        writer.write("<!DOCTYPE html>\n<html><head><title>");
        writer.write(escapeContent(documentLabel));
        writer.write("</title></head><body><h1>");
        googleoffIndex();
        // TODO(ejona): Localize.
        writer.write(computeTypeHeaderLabel(type));
        googleonIndex();
        writer.write(" ");
        writer.write(escapeContent(documentLabel));
        writer.write("</h1>");
        state = State.STARTED;
    }

    public void startSection(ObjectType type) throws IOException {
        if (state != State.STARTED && state != State.IN_SECTION) {
            throw new IllegalStateException("In unexpected state: " + state);
        }
        checkAndCloseSection();
        writer.write("<p>");
        googleoffIndex();
        writer.write(escapeContent(computeTypeSectionLabel(type)));
        googleonIndex();
        writer.write("</p><ul>");
        state = State.IN_SECTION;
    }

    private void checkAndCloseSection() throws IOException {
        if (state == State.IN_SECTION) {
            writer.write("</ul>");
        }
    }

    /**
     * @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.IN_SECTION) {
            throw new IllegalStateException("In unexpected state: " + state);
        }
        if (doc == null) {
            throw new NullPointerException();
        }
        if (countingOutputStream.getCount() + POSSIBLY_BUFFERED_BYTES > thresholdBytes) {
            overflowDocIds.add(doc);
        }
        writer.write("<li><a href=\"");
        writer.write(escapeAttributeValue(encodeDocId(doc)));
        writer.write("\">");
        writer.write(escapeContent(computeLabel(label, doc)));
        writer.write("</a></li>");
    }

    private void addComment(String comment) throws IOException {
        writer.write("<!--");
        writer.write(escapeContent(comment));
        writer.write("-->");
    }

    private void googleoffIndex() throws IOException {
        addComment("googleoff: index");
    }

    private void googleonIndex() throws IOException {
        addComment("googleon: index");
    }

    public void addMetadata(Multimap<String, String> metadata) throws IOException {
        checkAndCloseSection();
        googleoffIndex();
        writer.write("<table style='border: none'>");
        for (Map.Entry<String, String> me : metadata.entries()) {
            writer.write("<tr><td>");
            writer.write(escapeContent(me.getKey()));
            writer.write("</td><td>");
            writer.write(escapeContent(me.getValue()));
            writer.write("</td></tr>");
        }
        writer.write("</table>");
        googleonIndex();
        state = State.STARTED;
    }

    /**
     * Complete HTML body and flush.
     */
    public void finish() throws IOException {
        log.entering("HtmlResponseWriter", "finish");
        if (state != State.STARTED && state != State.IN_SECTION) {
            throw new IllegalStateException("In unexpected state: " + state);
        }
        if (!overflowDocIds.isEmpty()) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        pusher.pushDocIds(overflowDocIds);
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    }
                }
            });
        }
        checkAndCloseSection();
        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 (label == null || "".equals(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 computeTypeHeaderLabel(ObjectType type) {
        // TODO(ejona): Localize.
        switch (type) {
        case VIRTUAL_SERVER:
            return "Virtual Server";
        case SITE:
            return "Site";
        case LIST:
            return "List";
        case FOLDER:
            return "Folder";
        case LIST_ITEM:
            return "List Item";
        default:
            log.log(Level.WARNING, "Unexpected ObjectType: {0}", type);
            return "";
        }
    }

    private String computeTypeSectionLabel(ObjectType type) {
        // TODO(ejona): Localize.
        switch (type) {
        case SITE:
            return "Sites";
        case LIST:
            return "Lists";
        case FOLDER:
            return "Folders";
        case LIST_ITEM:
            return "List Items";
        case LIST_ITEM_ATTACHMENTS:
            return "Attachments";
        default:
            log.log(Level.WARNING, "Unexpected ObjectType: {0}", type);
            return "";
        }
    }

    private String escapeContent(String raw) {
        return raw.replace("&", "&amp;").replace("<", "&lt;");
    }

    private String escapeAttributeValue(String raw) {
        return escapeContent(raw).replace("\"", "&quot;").replace("'", "&apos;");
    }
}