/** * Copyright 2009 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 org.waveprotocol.box.consoleclient; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import org.waveprotocol.box.common.DocumentConstants; import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.DocInitializationCursor; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.impl.InitializationCursorAdapter; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.BlipData; import org.waveprotocol.wave.model.wave.data.WaveletData; import org.waveprotocol.wave.util.logging.Log; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * A {@link ClientWaveView} wrapper that can be rendered and scrolled for the * console. */ public class ScrollableWaveView extends ConsoleScrollable { private static final Log LOG = Log.get(ScrollableWaveView.class); public enum RenderMode { NORMAL, XML } /** * Wave we are wrapping. */ private final ClientWaveView wave; /** * Render mode. */ private RenderMode renderMode = RenderMode.NORMAL; /** * Create new scrollable wave view. * * @param wave to render */ public ScrollableWaveView(ClientWaveView wave) { this.wave = wave; } /** * @return the wrapped wave view */ public ClientWaveView getWave() { return wave; } @Override public synchronized List<String> render(final int width, final int height) { List<String> lines = Lists.newArrayList(); WaveletData convRoot = ClientUtils.getConversationRoot(wave); renderManifest(convRoot, width, lines); // Also render a header, not too big... List<String> header = renderHeader(width); while (header.size() > height / 2) { header.remove(header.size() - 1); } // In this case, we actually want to scroll from the bottom. Collections.reverse(lines); List<String> reverseScroll = scroll(height - header.size(), lines); Collections.reverse(reverseScroll); ConsoleUtils.ensureHeight(width, height - header.size(), reverseScroll); header.addAll(reverseScroll); return header; } private void renderDocument(BlipData document, final int width, final List<String> lines, final StringBuilder currentLine, final String padding) { DocOp docOp = document.getContent().asOperation(); docOp.apply(InitializationCursorAdapter.adapt( new DocInitializationCursor() { final Deque<String> elemStack = new LinkedList<String>(); @Override public void characters(String s) { if (elemStack.getLast().equals("w:image")) { currentLine.append( "(image, caption=" + ConsoleUtils.renderNice(s) + ")"); } else { currentLine.append(padding + ConsoleUtils.renderNice(s)); } wrap(lines, width, currentLine); } @Override public void elementStart(String type, Attributes attrs) { elemStack.push(type); if (renderMode.equals(RenderMode.NORMAL)) { if (type.equals(DocumentConstants.LINE)) { outputCurrentLine(lines, width, currentLine); } else if (type.equals(DocumentConstants.CONTRIBUTOR)) { if (attrs.containsKey(DocumentConstants.CONTRIBUTOR_NAME)) { displayAuthor(attrs.get(DocumentConstants.CONTRIBUTOR_NAME)); } } else if (type.equals(DocumentConstants.BODY)) { // Ignore. } else { LOG.warning("Unsupported element type while rendering document: " + type); } } else if (renderMode.equals(RenderMode.XML)) { for (int i = 0; i < elemStack.size() - 1; i++) { currentLine.append(" "); } if (attrs.isEmpty()) { currentLine.append("<" + type + ">"); } else { currentLine.append("<" + type + " "); for (String key : attrs.keySet()) { currentLine.append(key + "=\"" + attrs.get(key) + "\""); } currentLine.append(">"); } outputCurrentLine(lines, width, currentLine); } } @Override public void elementEnd() { String type = elemStack.pop(); if (renderMode.equals(RenderMode.XML)) { for (int i = 0; i < elemStack.size(); i++) { currentLine.append(" "); } currentLine.append("</" + type + ">"); outputCurrentLine(lines, width, currentLine); } } @Override public void annotationBoundary(AnnotationBoundaryMap map) { } private void displayAuthor(String author) { ConsoleUtils.ensureWidth(width, currentLine); lines.add(currentLine.toString()); currentLine.delete(0, currentLine.length() - 1); lines.add(ConsoleUtils.blankLine(width)); lines.add(ConsoleUtils.ansiWrap( ConsoleUtils.ANSI_GREEN_FG, ConsoleUtils.ensureWidth(width, author))); } })); } private void renderManifest(final WaveletData waveletData, final int width, final List<String> lines) { BlipData manifest = waveletData.getDocument("conversation"); Preconditions.checkArgument(manifest != null); final StringBuilder currentLine = new StringBuilder(); if (renderMode.equals(RenderMode.XML)) { // Only render the manifest XML itself if we are rendering the XML. // TODO: refactor the XML rendering code to not share these methods (it should just iterate // through all documents and render the XML, ignoring the conversation model). renderDocument(manifest, width, lines, currentLine, ""); } DocOp docOp = manifest.getContent().asOperation(); docOp.apply(InitializationCursorAdapter.adapt( new DocInitializationCursor() { final Deque<String> elemStack = new LinkedList<String>(); private int threadDepth; @Override public void characters(String s) { // Ignore characters in a manifest. } @Override public void elementStart(String type, Attributes attrs) { elemStack.push(type); if (renderMode.equals(RenderMode.NORMAL)) { if (type.equals(DocumentConstants.BLIP)) { if (attrs.containsKey(DocumentConstants.BLIP_ID)) { BlipData document = waveletData.getDocument(attrs.get(DocumentConstants.BLIP_ID)); if (document == null) { // A nonexistent document is indistinguishable from the empty document, so this // is not necessarily an error. } else { StringBuilder paddingBuilder = new StringBuilder(); for (int i = 0; i < threadDepth; i++) { paddingBuilder.append(" "); } String padding = paddingBuilder.toString(); displayAuthor(padding + "Blip: " + attrs.get(DocumentConstants.BLIP_ID)); renderDocument(document, width, lines, currentLine, padding); outputCurrentLine(lines, width, currentLine); } } } else if (type.equals(DocumentConstants.THREAD)) { threadDepth++; } else if (type.equals(DocumentConstants.CONVERSATION)) { // There should be a toplevel conversation element in every manifest. } else { LOG.warning("Unsupported element type while rendering manifest: " + type); } } else if (renderMode.equals(RenderMode.XML)) { if (type.equals(DocumentConstants.BLIP)) { if (attrs.containsKey(DocumentConstants.BLIP_ID)) { lines.add(ConsoleUtils.ansiWrap( ConsoleUtils.ANSI_BLUE_FG, "<!-- document named: " + attrs.get(DocumentConstants.BLIP_ID) + " -->")); BlipData document = waveletData.getDocument(attrs.get(DocumentConstants.BLIP_ID)); if (document == null) { // A nonexistent document is indistinguishable from the empty document, so this // is not necessarily an error. } else { renderDocument(document, width, lines, currentLine, ""); } } } else if (type.equals(DocumentConstants.THREAD)) { threadDepth++; } } } @Override public void elementEnd() { String type = elemStack.pop(); if (type.equals(DocumentConstants.THREAD)) { threadDepth--; } } @Override public void annotationBoundary(AnnotationBoundaryMap map) { } private void displayAuthor(String author) { ConsoleUtils.ensureWidth(width, currentLine); lines.add(currentLine.toString()); currentLine.delete(0, currentLine.length() - 1); lines.add(ConsoleUtils.blankLine(width)); lines.add(ConsoleUtils.ansiWrap(ConsoleUtils.ANSI_GREEN_FG, ConsoleUtils.ensureWidth(width, author))); } })); } private void outputCurrentLine(List<String> lines, int width, StringBuilder currentLine) { wrap(lines, width, currentLine); lines.add(currentLine.toString()); currentLine.delete(0, currentLine.length()); } /** * Render a header, containing extra information about the participants. * * @param width of the header * @return list of lines that make up the header */ private List<String> renderHeader(int width) { List<String> lines = Lists.newArrayList(); Set<ParticipantId> participants = ClientUtils.getConversationRoot(wave).getParticipants(); // HashedVersion StringBuilder versionLineBuilder = new StringBuilder(); versionLineBuilder.append("Version " + wave.getWaveletVersion(ClientUtils.getConversationRootId(wave))); wrapAndClose(lines, width, versionLineBuilder); // Participants StringBuilder participantLineBuilder = new StringBuilder(); if (participants.isEmpty()) { participantLineBuilder.append("No participants!?"); } else { Iterator<ParticipantId> it = participants.iterator(); participantLineBuilder.append("With "); participantLineBuilder.append(it.next()); while(it.hasNext()){ participantLineBuilder.append(", "); participantLineBuilder.append(it.next()); } } // Render as lines. wrapAndClose(lines, width, participantLineBuilder); for (int i = 0; i < lines.size(); i++) { lines.set(i, ConsoleUtils.ansiWrap(ConsoleUtils.ANSI_YELLOW_FG, lines.get(i))); } lines.add(ConsoleUtils.ensureWidth(width, "----")); return lines; } /** * Wrap a line by continually removing characters from a string and adding to * a list of lines, until the line is shorter than width. * * @param lines to append the wrapped string to * @param width to wrap * @param line to wrap */ private void wrap(List<String> lines, int width, StringBuilder line) { while (line.length() >= width) { lines.add(line.substring(0, width)); line.delete(0, width); } } /** * Wrap a line as in {@code wrap}, then "close" it by adding any remaining * characters to the list of lines and clearing the line. * * @param lines to append line to * @param width to wrap * @param line to append */ private void wrapAndClose(List<String> lines, int width, StringBuilder line) { wrap(lines, width, line); if (line.length() > 0) { lines.add(ConsoleUtils.ensureWidth(width, line.toString())); line.delete(0, line.length()); } } /** * @return the current rendering mode */ public RenderMode getRenderingMode() { return renderMode; } /** * Set rendering mode. * * @param mode for rendering */ public void setRenderingMode(RenderMode mode) { this.renderMode = mode; } }