package org.buddycloud.channelserver.channel.validate; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import org.apache.log4j.Logger; import org.buddycloud.channelserver.channel.ChannelManager; import org.buddycloud.channelserver.channel.Conf; import org.buddycloud.channelserver.db.exception.NodeStoreException; import org.buddycloud.channelserver.pubsub.model.GlobalItemID; import org.buddycloud.channelserver.pubsub.model.NodeItem; import org.buddycloud.channelserver.pubsub.model.impl.GlobalItemIDImpl; import org.buddycloud.channelserver.utils.XMLConstants; import org.buddycloud.channelserver.utils.node.item.payload.ActivityStreams; import org.buddycloud.channelserver.utils.node.item.payload.Atom; import org.buddycloud.channelserver.utils.node.item.payload.Buddycloud; import org.dom4j.Element; import org.dom4j.dom.DOMElement; import org.xmpp.packet.JID; public class AtomEntry implements PayloadValidator { public static final String MISSING_CONTENT_ELEMENT = "content-element-required"; public static final String MISSING_ENTRY_ELEMENT = "entry-element-required"; public static final String UNSUPPORTED_CONTENT_TYPE = "unsupported-content-type"; public static final String MAX_THREAD_DEPTH_EXCEEDED = "max-thread-depth-exceeded"; public static final String PARENT_ITEM_NOT_FOUND = "parent-item-not-found"; public static final String TARGETED_ITEM_NOT_FOUND = "targeted-item-not-found"; public static final String IN_REPLY_TO_MISSING = "missing-in-reply-to"; public static final String TARGET_ELEMENT_MISSING = "missing-target"; public static final String MISSING_TARGET_ID = "missing-target-id"; public static final String RATING_OUT_OF_RANGE = "rating-out-of-range"; public static final String INVALID_RATING_VALUE = "invalid-rating"; public static final String TARGET_MUST_BE_IN_SAME_THREAD = "target-outside-thread"; public static final String CAN_ONLY_RATE_A_POST = "invalid-rating-request"; public static final String ITEM_ALREADY_RATED = "item-already-rated"; public static final String CONTENT_TEXT = "text"; public static final String CONTENT_XHTML = "xhtml"; public static final String AUTHOR_URI_PREFIX = "acct:"; public static final String AUTHOR_TYPE = "person"; public static final String POST_TYPE_NOTE = "note"; public static final String POST_TYPE_COMMENT = "comment"; public static final String ACTIVITY_VERB_POST = "post"; public static final String ACTIVITY_VERB_RATED = "rated"; private static final Logger LOGGER = Logger.getLogger(Atom.class); private Element entry; private String errorMessage = ""; private String inReplyTo = null; private String targetId = null; private int itemRating = 0; private Element meta; private Element media; private JID jid; private String channelServerDomain; private String node; private ChannelManager channelManager; private NodeItem replyingToItem; private NodeItem targetItem; Map<String, String> params = new HashMap<String, String>(); private Element geoloc; private String globalItemID; public AtomEntry() { } public AtomEntry(Element item) { setPayload(item); } @Override public void setPayload(Element item) { if (null != item) { this.entry = item.element("entry"); } } @Override public String getErrorMessage() { return this.errorMessage; } @Override public void setChannelManager(ChannelManager channelManager) { this.channelManager = channelManager; } @Override public boolean isValid() throws NodeStoreException { if (this.entry == null) { this.errorMessage = MISSING_ENTRY_ELEMENT; return false; } Element id = this.entry.element(XMLConstants.ID_ELEM); if ((id == null) || (id.getText().isEmpty())) { if (null != id) { id.detach(); } LOGGER.debug("ID of the entry was missing. We add a default one to it: 1"); this.entry.addElement("id").setText("1"); } Element title = this.entry.element(XMLConstants.TITLE_ELEM); if (null == title) { LOGGER.debug("Title of the entry was missing. We add a default one to it: 'Post'."); title = this.entry.addElement(XMLConstants.TITLE_ELEM); title.setText("Post"); } this.params.put(XMLConstants.TITLE_ELEM, title.getText()); Element content = this.entry.element("content"); if (null == content) { this.errorMessage = MISSING_CONTENT_ELEMENT; return false; } String contentType = content.attributeValue(XMLConstants.TYPE_ATTR); if (null == contentType) { contentType = CONTENT_TEXT; } if ((!contentType.equals(CONTENT_TEXT)) && (!contentType.equals(CONTENT_XHTML))) { this.errorMessage = UNSUPPORTED_CONTENT_TYPE; return false; } this.params.put("content", content.getText()); this.params.put("content-type", contentType); Element updated = this.entry.element(XMLConstants.UPDATED_ELEM); if (null == updated) { String updateTime = Conf.formatDate(new Date()); LOGGER.debug("Update of the entry was missing. We add a default one to it: '" + updateTime + "'."); this.entry.addElement(XMLConstants.UPDATED_ELEM).setText(updateTime); } this.geoloc = this.entry.element(XMLConstants.GEOLOC_ELEM); if (!validateInReplyToElement(this.entry.element(XMLConstants.IN_REPLY_TO_ELEM))) { return false; } if (!validateTargetElement(this.entry.element(XMLConstants.TARGET_ELEM))) { return false; } if (!validateRatingElement(this.entry.element(XMLConstants.RATING_ELEM))) { return false; } Element meta = this.entry.element(XMLConstants.META_ELEM); if (null != meta) { this.meta = meta; } Element media = this.entry.element(XMLConstants.MEDIA_ELEM); if (null != media) { this.media = media; } return true; } @Override public Element getPayload() { Element entry = new DOMElement(XMLConstants.ENTRY_ELEM, new org.dom4j.Namespace("", Atom.NS)); entry.add(new org.dom4j.Namespace("activity", ActivityStreams.NS)); String postType = POST_TYPE_NOTE; String activityVerb = ACTIVITY_VERB_POST; entry.addElement(XMLConstants.ID_ELEM).setText(getGlobalItemId()); String title = this.params.get("title"); String itemContent = this.params.get("content"); String publishedDate = Conf.formatDate(new Date()); entry.addElement(XMLConstants.PUBLISHED_ELEM).setText(publishedDate); entry.addElement(XMLConstants.UPDATED_ELEM).setText(publishedDate); Element author = entry.addElement(XMLConstants.AUTHOR_ELEM); author.addElement(XMLConstants.NAME_ELEM).setText(jid.toBareJID()); author.addElement(XMLConstants.URI_ELEM).setText(AUTHOR_URI_PREFIX + jid.toBareJID()); author.addElement(XMLConstants.ACTIVITY_OBJECT_TYPE_ELEM).setText(AUTHOR_TYPE); if (this.geoloc != null) { entry.add(this.geoloc.createCopy()); } if (this.inReplyTo != null) { Element reply = entry.addElement(XMLConstants.IN_REPLY_TO_ELEM); reply.addNamespace("", Atom.NS_THREAD); reply.addAttribute("ref", inReplyTo); postType = POST_TYPE_COMMENT; } this.geoloc = this.entry.element("geoloc"); if (null != meta) { entry.add(meta.createCopy()); } if (null != media) { entry.addNamespace(Buddycloud.NS_MEDIA_PREFIX, Buddycloud.NS_MEDIA); Element mediaElement = media.createCopy(); for (Iterator<Element> iter = mediaElement.elements().iterator(); iter.hasNext();) { Element item = iter.next(); item.setName(Buddycloud.NS_MEDIA_PREFIX + ":item"); } mediaElement.setName(Buddycloud.NS_MEDIA_PREFIX + ":media"); entry.add(mediaElement); } if (null != targetId) { GlobalItemIDImpl globalTargetId = new GlobalItemIDImpl(new JID(channelServerDomain), node, targetId); Element target = entry.addElement("activity:target"); target.addElement("id").setText(globalTargetId.toString()); target.addElement("activity:object-type").setText("post"); } if (itemRating > 0) { entry.addNamespace("review", ActivityStreams.NS_REVIEW); String rating = String.format("%d.0", itemRating); entry.addElement("review:rating").setText(rating); title = "Rating"; itemContent = "rating:" + rating; activityVerb = ACTIVITY_VERB_RATED; } entry.addElement("title").setText(title); Element content = entry.addElement("content"); content.setText(itemContent); content.addAttribute("type", this.params.get("content-type")); entry.addElement("activity:verb").setText(activityVerb); Element activityObject = entry.addElement("activity:object"); activityObject.addElement("activity:object-type").setText(postType); return entry; } @Override public void setUser(JID jid) { this.jid = jid; } @Override public void setNode(String node) { this.node = node; } @Override public void setTo(String channelServerDomain) { this.channelServerDomain = channelServerDomain; } private boolean validateInReplyToElement(Element reply) throws NodeStoreException { if (null == reply) { return true; } inReplyTo = reply.attributeValue("ref"); if (GlobalItemIDImpl.isGlobalId(inReplyTo)) { inReplyTo = GlobalItemIDImpl.toLocalId(inReplyTo); } replyingToItem = channelManager.getNodeItem(node, inReplyTo); if (null == replyingToItem) { this.errorMessage = PARENT_ITEM_NOT_FOUND; return false; } if (null != replyingToItem.getInReplyTo()) { LOGGER.error("User is attempting to reply to a reply"); this.errorMessage = MAX_THREAD_DEPTH_EXCEEDED; return false; } return true; } private boolean validateTargetElement(Element target) throws NodeStoreException { if (null == target) { return true; } targetId = target.elementText(XMLConstants.ID_ELEM); if ((null == targetId) || (0 == targetId.length())) { this.errorMessage = MISSING_TARGET_ID; return false; } if (null == inReplyTo) { this.errorMessage = IN_REPLY_TO_MISSING; return false; } if (GlobalItemIDImpl.isGlobalId(targetId)) { targetId = GlobalItemIDImpl.toLocalId(targetId); } if (targetId.equals(replyingToItem.getId())) { targetItem = replyingToItem; } else { targetItem = channelManager.getNodeItem(node, targetId); } if (null == targetItem) { this.errorMessage = TARGETED_ITEM_NOT_FOUND; return false; } if (targetItem.getId().equals(targetId)) { return true; } if ((null == targetItem.getInReplyTo()) || (!targetItem.getInReplyTo().equals(targetId))) { this.errorMessage = TARGET_MUST_BE_IN_SAME_THREAD; return false; } return true; } private boolean validateRatingElement(Element rating) throws NodeStoreException { if (null == rating) { return true; } if (null == inReplyTo) { this.errorMessage = IN_REPLY_TO_MISSING; return false; } if (null == targetId) { this.errorMessage = TARGET_ELEMENT_MISSING; return false; } if (targetItem.getPayload().indexOf(ActivityStreams.NS_REVIEW) > -1) { this.errorMessage = CAN_ONLY_RATE_A_POST; return false; } try { double itemRatingFloat = Double.parseDouble(rating.getTextTrim()); if (itemRatingFloat != Math.floor(itemRatingFloat)) { throw new NumberFormatException("Non-integer rating provided"); } itemRating = (int) itemRatingFloat; } catch (NumberFormatException e) { this.errorMessage = INVALID_RATING_VALUE; return false; } if ((itemRating < 1) || (itemRating > 5)) { this.errorMessage = RATING_OUT_OF_RANGE; return false; } GlobalItemID globalTargetId = new GlobalItemIDImpl(new JID(channelServerDomain), node, targetId); if (channelManager.userHasRatedPost(node, jid, globalTargetId)) { this.errorMessage = ITEM_ALREADY_RATED; return false; } return true; } @Override public String getLocalItemId() { return GlobalItemIDImpl.toLocalId(getGlobalItemId()); } @Override public String getGlobalItemId() { if (null == globalItemID) { String id = UUID.randomUUID().toString(); globalItemID = new GlobalItemIDImpl(new JID(channelServerDomain), node, id).toString(); } return globalItemID; } @Override public String getInReplyTo() { String localReply = null; Element inReplyToElement = this.entry.element(XMLConstants.IN_REPLY_TO_ELEM); if (null != inReplyToElement) { localReply = GlobalItemIDImpl.toLocalId(inReplyToElement.attributeValue("ref")); } return localReply; } @Override public String[] capabilities() { return new String[] {null, Atom.NS}; } public boolean canValidate(String contentType) { return Arrays.asList(capabilities()).contains(contentType); } }