/* Copyright (c) 2008 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.gdata.model.atom;
import com.google.gdata.util.common.base.StringUtil;
import com.google.common.collect.ImmutableMap;
import com.google.gdata.util.common.html.HtmlToText;
import com.google.gdata.client.CoreErrorDomain;
import com.google.gdata.client.Service;
import com.google.gdata.data.IContent;
import com.google.gdata.data.ITextConstruct;
import com.google.gdata.data.ITextContent;
import com.google.gdata.data.TextConstruct;
import com.google.gdata.model.Element;
import com.google.gdata.model.ElementCreator;
import com.google.gdata.model.ElementKey;
import com.google.gdata.model.ElementMetadata;
import com.google.gdata.model.ElementValidator;
import com.google.gdata.model.MetadataRegistry;
import com.google.gdata.model.QName;
import com.google.gdata.model.ValidationContext;
import com.google.gdata.model.XmlBlob;
import com.google.gdata.util.Namespaces;
/**
* Variant of {@link Content} for entries containing text.
*/
public class TextContent extends Content
implements ITextContent, ITextConstruct {
/** The kind name for adaptation. */
public static final String KIND = "text";
/**
* The key for TextContent used as a construct. This will apply to all uses
* of TextContent regardless of QName.
*/
@SuppressWarnings("hiding")
public static final ElementKey<String, TextContent> CONSTRUCT =
ElementKey.of(null, String.class, TextContent.class);
/**
* The key for atom:content when it contains TextContent.
*/
@SuppressWarnings("hiding")
public static final ElementKey<String, TextContent> KEY = ElementKey.of(
Content.KEY.getId(), String.class, TextContent.class);
/**
* The key for xhtml:div.
*/
public static final ElementKey<String, XmlBlob> DIV = ElementKey.of(
new QName(Namespaces.xhtmlNs, "div"), String.class, XmlBlob.class);
/**
* Registers the metadata for this element.
*/
public static void registerMetadata(MetadataRegistry registry) {
if (registry.isRegistered(CONSTRUCT)) {
return;
}
Content.registerMetadata(registry);
ElementCreator constructBuilder = registry.build(CONSTRUCT)
.setValidator(new TextContentValidator());
constructBuilder.addElement(DIV);
ElementCreator builder = registry.build(KEY);
registry.adapt(Content.KEY, KIND, KEY);
}
// Unknown type attributes will map to this value.
private static final int UNKNOWN_TYPE = -1;
// A map of type attribute values to content types.
private static final ImmutableMap<String, Integer> TYPE_MAP =
ImmutableMap.<String, Integer>builder()
.put("plain", IContent.Type.TEXT)
.put("text", IContent.Type.TEXT)
.put("text/plain", IContent.Type.TEXT)
.put("html", IContent.Type.HTML)
.put("text/html", IContent.Type.HTML)
.put("xhtml", IContent.Type.XHTML)
.build();
/**
* Validator for {@link TextContent}.
*/
private static class TextContentValidator implements ElementValidator {
/**
* Validates that plain text or html text content has a text value and no
* nested elements, and validates that xhtml contains a single DIV element.
* <p>
* Also adds an error if the tyep attribute on the given element is unknown.
*/
public void validate(ValidationContext vc, Element e,
ElementMetadata<?, ?> metadata) {
int type = getType(e);
switch (type) {
case UNKNOWN_TYPE:
vc.addError(e,
CoreErrorDomain.ERR.invalidTextContentType.withInternalReason(
"Invalid type: " + type));
break;
case IContent.Type.TEXT:
case IContent.Type.HTML:
if (!e.hasTextValue()) {
vc.addError(e, CoreErrorDomain.ERR.missingTextContent);
}
if (e.getElementCount() != 0) {
vc.addError(e,
CoreErrorDomain.ERR.invalidChildElement.withInternalReason(
"Child elements not allowed on text content"));
}
break;
case IContent.Type.XHTML:
if (!e.hasElement(DIV)) {
vc.addError(e,
CoreErrorDomain.ERR.missingExtensionElement.withInternalReason(
"xhtml text content must have a div element"));
} else if (e.getElementCount() != 1) {
vc.addError(e,
CoreErrorDomain.ERR.invalidChildElement.withInternalReason(
"xhtml must only have a single child element"));
}
break;
default:
throw new IllegalStateException("Shouldn't be possible, "
+ "TYPE_MAP can only map to text, html, or xhtml.");
}
}
}
/**
* Creates a text content. This method is convenient for some service
* implementations. Note that XHTML is treated somewhat differently from text
* and HTML, as it is stored as a separate element instead of in the value
* portion of this element.
*
* @param type
* the type of the new text construct (TEXT, HTML, or XHTML)
*
* @param textOrHtml
* the contents to put in this text construct, if the type is
* TEXT or HTML.
* If type is XHTML, set this parameter to {@code null}.
*
* @param xhtml
* the contents to put in this text construct, if the type is
* XHTML.
* If type is TEXT or HTML, set this parameter to {@code null}.
*
* @return a {@link TextConstruct} of the appropriate type.
*/
public static TextContent create(int type, String textOrHtml, XmlBlob xhtml) {
switch (type) {
case IContent.Type.TEXT:
return plainText(textOrHtml);
case IContent.Type.HTML:
return html(textOrHtml);
case IContent.Type.XHTML:
return xhtml(xhtml);
default:
throw new IllegalArgumentException("Invalid type: " + type);
}
}
/**
* Construct a new plain text content with the given text.
*/
public static TextContent plainText(String text) {
TextContent content = new TextContent();
content.setText(text);
return content;
}
/**
* Construct a new html text content with the given html.
*/
public static TextContent html(String html) {
TextContent content = new TextContent();
content.setHtml(html);
return content;
}
/**
* Construct a new Xhtml text content from the given div.
*/
public static TextContent xhtml(XmlBlob div) {
TextContent content = new TextContent();
content.setXhtml(div);
return content;
}
/**
* Returns the int value (old school enum) for the type attribute on the
* given element. This method will return {@link #UNKNOWN_TYPE} if the type
* attribute is unknown, otherwise it will return {@link Content.Type#TEXT}
* for plain text, {@link Content.Type#HTML} for html, or
* {@link Content.Type#XHTML} for XHTML content.
*/
private static int getType(Element e) {
String type = e.getAttributeValue(Content.TYPE);
Integer typeVal = (type == null) ? IContent.Type.TEXT : TYPE_MAP.get(type);
return (typeVal == null) ? UNKNOWN_TYPE : typeVal.intValue();
}
/**
* Constructs a new plain text instance using the default key.
*/
public TextContent() {
super(CONSTRUCT);
}
/**
* Constructs a new instance using the specified key.
*
* @param key the element key for this element
*/
protected TextContent(ElementKey<?, ?> key) {
super(key);
}
/**
* Constructs a new instance from a more generic {@link Content} type.
*
* @param key the element key to use for this instance
* @param content generic content
*/
protected TextContent(ElementKey<?, ?> key, Content content) {
super(key, content);
}
/**
* Returns the type of this content, either {@link Content.Type#TEXT},
* {@link Content.Type#HTML}, or {@link Content.Type#XHTML}. If the value
* of the {@link #TYPE} attribute is unknown, plain text
* {@link Content.Type#TEXT} will be returned.
*/
@Override
public int getType() {
int type = getType(this);
return (type == UNKNOWN_TYPE) ? IContent.Type.TEXT : type;
}
/**
* Returns {@code true} if there is no content element for this text content.
*/
public boolean isEmpty() {
return StringUtil.isEmpty(getText()) && getElementCount() == 0;
}
/**
* Returns a plain-text representation of this text content. For content that
* is already plain text, this just returns the text value, but for html and
* xhtml it first converts the data into a plain text representation before
* returning it.
*/
public String getPlainText() {
switch (getType()) {
case IContent.Type.XHTML:
return getXhtml().getBlob();
case IContent.Type.TEXT:
return getText();
case IContent.Type.HTML:
return HtmlToText.htmlToPlainText(getText());
default:
throw new IllegalStateException("Shouldn't be possible, "
+ "getType can only return TEXT, HTML, or XHTML.");
}
}
/**
* Returns the text content of this element, if this is a plain text or html
* text content. If this is an XHTML text content this method returns null.
* If you want a plain-text representation of the XHTML content call
* {@link #getPlainText()} instead.
*/
public String getText() {
return getTextValue(KEY);
}
/**
* Backwards-compatibility method, exactly the same as {@link #getText()}.
*/
public String getHtml() {
return getText();
}
/**
* Returns the XHTML content of this text content, or {@code null} if no such
* element exists.
*/
public XmlBlob getXhtml() {
// Non-xhtml types don't have a div element.
if (getType() != Content.Type.XHTML) {
return null;
}
XmlBlob div = getElement(DIV);
if (div == null) {
div = new XmlBlob(DIV);
setXhtml(div);
}
return div;
}
/**
* Specifies the text of this element, turning this into a plain-text
* content element if it wasn't already.
*/
public void setText(String text) {
if (Service.getVersion().isBefore(Service.Versions.V2)) {
setAttributeValue(Content.TYPE, "text");
} else {
setAttributeValue(Content.TYPE, null);
}
setTextValue(text);
}
/**
* Specifies the text of this element, turning this into an html text content
* element if it wasn't already.
*/
public void setHtml(String html) {
setAttributeValue(Content.TYPE, "html");
setTextValue(html);
}
/**
* Specifies the XHTML content of this element, turning this into an xhtml
* text content element if it wasn't already.
*/
public void setXhtml(XmlBlob div) {
setAttributeValue(Content.TYPE, "xhtml");
setElement(DIV, div);
}
/**
* {@inheritDoc}
*
* In the case of plain text or html content, we resolve a null content body
* to an empty string.
*/
@Override
public Element resolve(ElementMetadata<?,?> metadata, ValidationContext vc) {
int type = getType();
if (type == IContent.Type.TEXT
&& Service.getVersion().isBefore(Service.Versions.V2)
&& getAttributeValue(Content.TYPE) == null) {
setAttributeValue(Content.TYPE, "text");
}
if (type == IContent.Type.TEXT || type == IContent.Type.HTML) {
if (getTextValue() == null) {
setTextValue("");
}
}
return super.resolve(metadata, vc);
}
public ITextConstruct getContent() {
return this;
}
}