/** * 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 com.google.wave.api.data; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.wave.api.Attachment; import com.google.wave.api.Element; import com.google.wave.api.ElementType; import com.google.wave.api.FormElement; import com.google.wave.api.Gadget; import com.google.wave.api.Image; import com.google.wave.api.Installer; import com.google.wave.api.Line; import org.waveprotocol.wave.model.conversation.Blips; import org.waveprotocol.wave.model.document.Doc; import org.waveprotocol.wave.model.document.Doc.E; import org.waveprotocol.wave.model.document.Doc.N; import org.waveprotocol.wave.model.document.Document; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.id.IdConstants; import org.waveprotocol.wave.model.wave.Wavelet; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Class to support serializing Elements from and to XML. * * */ public abstract class ElementSerializer { // Two maps to easily look up what to serialize private static final Map<ElementType, ElementSerializer> typeToSerializer = Maps.newHashMap(); private static final Map<String, ElementSerializer> tagToSerializer = Maps.newHashMap(); private static final String CAPTION_TAG = "caption"; private static final String CLICK_TAG = "click"; private static final String ATTACHMENT_STR = "attachment"; private static final String CAPTION_STR = "caption"; /** The attachment URL regular expression */ private static final Pattern ATTACHMENT_URL_PATTERN = Pattern.compile( "attachment_url\\\"\\ value\\=\\\"([^\\\"]*)\\\""); /** The attachment MIME type regular expression */ private static final Pattern MIME_TYPE_PATTERN = Pattern.compile( "mime_type\\\"\\ value\\=\\\"([^\\\"]*)\\\""); /** Attachment Download Host URL */ private static String attachmentDownloadHostUrl = ""; public static void setAttachmentDownloadHostUrl(String attachmentDownloadHostUrl){ ElementSerializer.attachmentDownloadHostUrl = attachmentDownloadHostUrl; } public static XmlStringBuilder apiElementToXml(Element e) { ElementSerializer serializer = typeToSerializer.get(e.getType()); if (serializer == null) { return null; } return serializer.toXml(e); } public static Element xmlToApiElement(Document doc, Doc.E element, Wavelet wavelet) { if (element == null) { return null; } ElementSerializer serializer = tagToSerializer.get(doc.getTagName(element)); if (serializer == null) { return null; } return serializer.fromXml(doc, element, wavelet); } public static String tagNameForElementType(ElementType lookup) { ElementSerializer serializer = typeToSerializer.get(lookup); if (serializer != null) { return serializer.tagName; } return null; } public static Map<Integer, Element> serialize(Document doc, Wavelet wavelet) { Map<Integer, Element> result = Maps.newHashMap(); ApiView apiView = new ApiView(doc, wavelet); Doc.N node = Blips.getBody(doc); if (node != null) { // The node is the body; we're after its children node = doc.getFirstChild(node); } while (node != null) { E element = doc.asElement(node); if (element != null) { Element apiElement = xmlToApiElement(doc, element, wavelet); if (apiElement != null) { result.put(apiView.transformToTextOffset(doc.getLocation(element)), apiElement); } } node = doc.getNextSibling(node); } return result; } private static void register(ElementSerializer serializer) { typeToSerializer.put(serializer.elementType, serializer); tagToSerializer.put(serializer.tagName, serializer); } static { register(new ElementSerializer("label", ElementType.LABEL) { @Override public XmlStringBuilder toXml(Element e) { String value = e.getProperty("value"); if (value == null) { value = e.getProperty("defaultValue"); } return wrapWithContent(value, "for", e.getProperty("name")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { FormElement formElement = createFormElement(doc, element); formElement.setName(doc.getAttribute(element, "for")); if (doc.getFirstChild(element) != null) { formElement.setDefaultValue(doc.getData(doc.asText(doc.getFirstChild(element)))); formElement.setValue(doc.getData(doc.asText(doc.getFirstChild(element)))); } return formElement; } }); register(new ElementSerializer("input", ElementType.INPUT) { @Override public XmlStringBuilder toXml(Element e) { String value = e.getProperty("value"); if (value == null) { value = e.getProperty("defaultValue"); } return wrapWithContent(value, "name", e.getProperty("name")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { FormElement formElement = createFormElement( doc, element, doc.getAttribute(element, "submit")); // Set the text content. if (doc.getFirstChild(element) != null) { formElement.setValue(doc.getData(doc.asText(doc.getFirstChild(element)))); } return formElement; } }); register(new ElementSerializer("password", ElementType.PASSWORD) { @Override public XmlStringBuilder toXml(Element e) { String value = e.getProperty("value"); if (value == null) { value = e.getProperty("defaultValue"); } return wrap("name", e.getProperty("name"), "value", value); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { return createFormElement(doc, element, doc.getAttribute(element, "value")); } }); register(new ElementSerializer("textarea", ElementType.TEXTAREA) { @Override public XmlStringBuilder toXml(Element e) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); String value = e.getProperty("value"); if (isEmptyOrWhitespace(value)) { res.append(XmlStringBuilder.createEmpty().wrap(LineContainers.LINE_TAGNAME)); } else { Splitter splitter = Splitter.on("\n"); for (String paragraph : splitter.split(value)) { res.append(XmlStringBuilder.createEmpty().wrap(LineContainers.LINE_TAGNAME)); res.append(XmlStringBuilder.createText(paragraph)); } } return res.wrap("textarea", "name", e.getProperty("name")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { // Set the text content. We're doing a little mini textview here. StringBuilder value = new StringBuilder(); Doc.N node = doc.getFirstChild(element); boolean first = true; while (node != null) { Doc.T text = doc.asText(node); if (text != null) { value.append(doc.getData(text)); } Doc.E docElement = doc.asElement(node); if (docElement != null && doc.getTagName(docElement).equals(LineContainers.LINE_TAGNAME)) { if (first) { first = false; } else { value.append('\n'); } } node = doc.getNextSibling(node); } return createFormElement(doc, element, value.toString()); } }); register(new ElementSerializer("button", ElementType.BUTTON) { @Override public XmlStringBuilder toXml(Element element) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); res.append(XmlStringBuilder.createText(element.getProperty("value")).wrap(CAPTION_TAG)); res.append(XmlStringBuilder.createEmpty().wrap("events")); return res.wrap("button", "name", element.getProperty("name")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { FormElement formElement = createFormElement(doc, element); Doc.N firstChild = doc.getFirstChild(element); // Get the default value from the caption. if (firstChild != null && doc.getTagName(doc.asElement(firstChild)).equals(CAPTION_TAG) && doc.getFirstChild(doc.asElement(firstChild)) != null) { formElement.setDefaultValue(doc.getData(doc.asText(doc.getFirstChild( doc.asElement(firstChild))))); } // Get the value from the last click event. if (firstChild != null && doc.getNextSibling(firstChild) != null && doc.asElement(doc.getFirstChild(doc.getNextSibling(firstChild))) != null && doc.getTagName(doc.asElement(doc.getFirstChild(doc.getNextSibling( firstChild)))).equals(CLICK_TAG)) { formElement.setValue("clicked"); } else { formElement.setValue(formElement.getDefaultValue()); } return formElement; } }); register(new ElementSerializer("radiogroup", ElementType.RADIO_BUTTON_GROUP) { @Override public XmlStringBuilder toXml(Element e) { return wrap("name", e.getProperty("name")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { FormElement formElement = createFormElement( doc, element, doc.getAttribute(element, "value")); return formElement; } }); register(new ElementSerializer("radio", ElementType.RADIO_BUTTON) { @Override public XmlStringBuilder toXml(Element e) { return wrap("name", e.getProperty("name"), "group", e.getProperty("value")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { return new FormElement(getElementType(), doc.getAttribute(element, "name"), doc.getAttribute(element, "group")); } }); register(new ElementSerializer("check", ElementType.CHECK) { @Override public XmlStringBuilder toXml(Element e) { return wrap("name", e.getProperty("name"), "submit", e.getProperty("defaultValue"), "value", e.getProperty("value")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { FormElement formElement = createFormElement( doc, element, doc.getAttribute(element, "value")); formElement.setDefaultValue(doc.getAttribute(element, "submit")); return formElement; } }); register(new ElementSerializer("extension_installer", ElementType.INSTALLER) { @Override public XmlStringBuilder toXml(Element e) { return wrap("manifest", e.getProperty("manifest")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { Installer installer = new Installer(); installer.setManifest(doc.getAttribute(element, "manifest")); return installer; } }); register(new ElementSerializer("gadget", ElementType.GADGET) { @Override public XmlStringBuilder toXml(Element element) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); if (element.getProperties().containsKey("category")) { res.append(XmlStringBuilder.createEmpty().wrap( "category", "name", element.getProperty("category"))); } if (element.getProperties().containsKey("title")) { res.append(XmlStringBuilder.createEmpty().wrap( "title", "value", element.getProperty("title"))); } if (element.getProperties().containsKey("thumbnail")) { res.append(XmlStringBuilder.createEmpty().wrap( "thumbnail", "value", element.getProperty("thumbnail"))); } for (Map.Entry<String, String> property : element.getProperties().entrySet()) { if (property.getKey().equals("category") || property.getKey().equals("url") || property.getKey().equals("title") || property.getKey().equals("thumbnail") || property.getKey().equals("author")) { continue; } else if (property.getKey().equals("pref")) { res.append(XmlStringBuilder.createEmpty().wrap("pref", "value", property.getValue())); } else { res.append(XmlStringBuilder.createEmpty().wrap("state", "name", property.getKey(), "value", property.getValue())); } } List<String> attributes = Lists.newArrayList("url", element.getProperty("url")); if (element.getProperties().containsKey("author")) { attributes.add("author"); attributes.add(element.getProperty("author")); } if (element.getProperties().containsKey("ifr")) { attributes.add("ifr"); attributes.add(element.getProperty("ifr")); } String[] asArray = new String[attributes.size()]; attributes.toArray(asArray); return res.wrap("gadget", asArray); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { Gadget gadget = new Gadget(); gadget.setUrl(doc.getAttribute(element, "url")); String author = doc.getAttribute(element, "author"); if (author != null) { gadget.setAuthor(author); } String ifr = doc.getAttribute(element, "ifr"); if (ifr != null) { gadget.setIframe(ifr); } // TODO(user): Streamline this. Maybe use SchemaConstraints.java to // get a list of child elements or attributes, then automate this. E child = doc.asElement(doc.getFirstChild(element)); while (child != null) { if (doc.getTagName(child).equals("name")) { gadget.setProperty("name", doc.getAttribute(child, "value")); } else if (doc.getTagName(child).equals("title")) { gadget.setProperty("title", doc.getAttribute(child, "value")); } else if (doc.getTagName(child).equals("thumbnail")) { gadget.setProperty("thumbnail", doc.getAttribute(child, "value")); } else if (doc.getTagName(child).equals("pref")) { gadget.setProperty("pref", doc.getAttribute(child, "value")); } else if (doc.getTagName(child).equals("state")) { gadget.setProperty(doc.getAttribute(child, "name"), doc.getAttribute(child, "value")); } else if (doc.getTagName(child).equals("category")) { gadget.setProperty("category", doc.getAttribute(child, "name")); } child = doc.asElement(doc.getNextSibling(child)); } return gadget; } }); register(new ElementSerializer("img", ElementType.IMAGE) { @Override public XmlStringBuilder toXml(Element element) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); List<String> attributes = Lists.newArrayList("src", element.getProperty("url")); if (element.getProperty("width") != null) { attributes.add("width"); attributes.add(element.getProperty("width")); } if (element.getProperty("height") != null) { attributes.add("height"); attributes.add(element.getProperty("height")); } if (element.getProperty("caption") != null) { attributes.add("alt"); attributes.add(element.getProperty("caption")); } String[] asArray = new String[attributes.size()]; attributes.toArray(asArray); return res.wrap("img", asArray); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { Image image = new Image(); if (doc.getAttribute(element, "src") != null) { image.setUrl(doc.getAttribute(element, "src")); } if (doc.getAttribute(element, "alt") != null) { image.setCaption(doc.getAttribute(element, "alt")); } if (doc.getAttribute(element, "width") != null) { image.setWidth(Integer.parseInt(doc.getAttribute(element, "width"))); } if (doc.getAttribute(element, "height") != null) { image.setHeight(Integer.parseInt(doc.getAttribute(element, "height"))); } return image; } }); register(new ElementSerializer("image", ElementType.ATTACHMENT) { @Override public XmlStringBuilder toXml(Element element) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); if (element.getProperties().containsKey("attachmentId")) { if (element.getProperty(CAPTION_STR) != null) { res.append(XmlStringBuilder.createText(element.getProperty(CAPTION_STR)) .wrap("caption")); } return res.wrap("image", ATTACHMENT_STR, element.getProperty("attachmentId")); } return res; } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { Map<String, String> properties = Maps.newHashMap(); String attachmentId = doc.getAttribute(element, ATTACHMENT_STR); if (attachmentId != null) { properties.put(Attachment.ATTACHMENT_ID, attachmentId); } String caption = getCaption(doc, element); if (caption != null) { properties.put(Attachment.CAPTION, caption); } if (wavelet != null && attachmentId != null) { Document attachmentDataDoc = wavelet.getDocument(IdConstants.ATTACHMENT_METADATA_PREFIX + "+" + attachmentId); if (attachmentDataDoc != null) { String dataDocument = attachmentDataDoc.toXmlString(); if (dataDocument != null) { properties.put(Attachment.MIME_TYPE, extractValue(dataDocument, MIME_TYPE_PATTERN)); properties.put(Attachment.ATTACHMENT_URL, ElementSerializer.attachmentDownloadHostUrl + getAttachmentUrl(dataDocument)); } } } return new Attachment(properties, null); } private String getCaption(Document doc, E element) { N node = doc.getFirstChild(element); while (node != null) { E cElement = doc.asElement(node); if (cElement != null && doc.getTagName(cElement).equals(CAPTION_TAG) && doc.getFirstChild(cElement) != null) { return doc.getData(doc.asText(doc.getFirstChild(cElement))); } node = doc.getNextSibling(node); } return null; } }); register(new ElementSerializer(LineContainers.LINE_TAGNAME, ElementType.LINE) { @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { Line paragraph = new Line(); if (doc.getAttribute(element, "t") != null) { paragraph.setLineType(doc.getAttribute(element, "t")); } if (doc.getAttribute(element, "i") != null) { paragraph.setIndent(doc.getAttribute(element, "i")); } if (doc.getAttribute(element, "a") != null) { paragraph.setAlignment(doc.getAttribute(element, "a")); } if (doc.getAttribute(element, "d") != null) { paragraph.setDirection(doc.getAttribute(element, "d")); } return paragraph; } @Override public XmlStringBuilder toXml(Element element) { XmlStringBuilder res = XmlStringBuilder.createEmpty(); // A cast would be nice here, but unfortunately the element // gets deserialized as an actual Element Line line = new Line(element.getProperties()); List<String> attributes = Lists.newArrayList(); if (!isEmptyOrWhitespace(line.getLineType())) { attributes.add("t"); attributes.add(line.getLineType()); } if (!isEmptyOrWhitespace(line.getIndent())) { attributes.add("i"); attributes.add(line.getIndent()); } if (!isEmptyOrWhitespace(line.getAlignment())) { attributes.add("a"); attributes.add(line.getAlignment()); } if (!isEmptyOrWhitespace(line.getDirection())) { attributes.add("d"); attributes.add(line.getDirection()); } String[] asArray = new String[attributes.size()]; attributes.toArray(asArray); return res.wrap(LineContainers.LINE_TAGNAME, asArray); } }); register(new ElementSerializer(Blips.THREAD_INLINE_ANCHOR_TAGNAME, ElementType.INLINE_BLIP) { @Override public XmlStringBuilder toXml(Element e) { return XmlStringBuilder.createEmpty().wrap( Blips.THREAD_INLINE_ANCHOR_TAGNAME, Blips.THREAD_INLINE_ANCHOR_ID_ATTR, e.getProperty("id")); } @Override public Element fromXml(Document doc, E element, Wavelet wavelet) { return new Element(ElementType.INLINE_BLIP, ImmutableMap.of("id", doc.getAttribute(element, Blips.THREAD_INLINE_ANCHOR_ID_ATTR))); } }); } private final String tagName; private final ElementType elementType; protected abstract XmlStringBuilder toXml(Element e); protected abstract Element fromXml(Document doc, E element, Wavelet wavelet); public String getTagName() { return tagName; } public ElementType getElementType() { return elementType; } protected XmlStringBuilder wrap(String... attributes) { return XmlStringBuilder.createEmpty().wrap(tagName, attributes); } protected XmlStringBuilder wrapWithContent(String content, String... attributes) { if (Strings.isNullOrEmpty(content)) { return wrap(attributes); } return XmlStringBuilder.createText(content).wrap(tagName, attributes); } /** * Helper method to create a form element * @return a form element of the right type and with the right name and * optionally an initial value. */ protected FormElement createFormElement(Document doc, E element, String initialValue) { FormElement formElement = new FormElement(elementType, doc.getAttribute(element, "name")); if (initialValue != null) { formElement.setValue(initialValue); formElement.setDefaultValue(initialValue); } return formElement; } protected FormElement createFormElement(Document doc, E element) { return createFormElement(doc, element, null); } public ElementSerializer(String tagName, ElementType elementType) { this.tagName = tagName; this.elementType = elementType; } private static boolean isEmptyOrWhitespace(String value) { return value == null || CharMatcher.WHITESPACE.matchesAllOf(value); } private static String getAttachmentUrl(String dataDocument) { String rawURL = extractValue(dataDocument, ATTACHMENT_URL_PATTERN); return rawURL == null ? "" : rawURL.replace("&", "&"); } // TODO(user): move away from REGEX private static String extractValue(String dataDocument, Pattern pattern) { Matcher matcher = pattern.matcher(dataDocument); return matcher.find() ? matcher.group(1) : null; } }