Java tutorial
/** * Copyright 2010 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.livingstories.server.dataservices.entities; import com.google.livingstories.client.AssetContentItem; import com.google.livingstories.client.AssetType; import com.google.livingstories.client.BackgroundContentItem; import com.google.livingstories.client.BaseContentItem; import com.google.livingstories.client.ContentItemType; import com.google.livingstories.client.DataContentItem; import com.google.livingstories.client.DefaultContentItem; import com.google.livingstories.client.EventContentItem; import com.google.livingstories.client.Importance; import com.google.livingstories.client.Location; import com.google.livingstories.client.NarrativeContentItem; import com.google.livingstories.client.NarrativeType; import com.google.livingstories.client.PlayerContentItem; import com.google.livingstories.client.PlayerType; import com.google.livingstories.client.PublishState; import com.google.livingstories.client.QuoteContentItem; import com.google.livingstories.client.ReactionContentItem; import com.google.livingstories.client.StoryPlayerContentItem; import com.google.livingstories.client.util.GlobalUtil; import com.google.livingstories.server.rpcimpl.ContentRpcImpl; import com.google.livingstories.server.util.TimeUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.Serializable; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.jdo.JDOException; import javax.jdo.annotations.Embedded; import javax.jdo.annotations.EmbeddedOnly; import javax.jdo.annotations.IdGeneratorStrategy; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; /** * This class represents a piece of content for a living story. */ @PersistenceCapable(identityType = IdentityType.APPLICATION) public class BaseContentEntity implements Serializable, JSONSerializable, HasSerializableLivingStoryId { private static final Pattern EXTERNAL_LINK_PATTERN = Pattern .compile("<a\\b[^>]+?\\bhref=\"(?!javascript:)[^>]+?>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final Pattern TARGET_ATTR_PATTERN = Pattern.compile("\\btarget=", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final String DEFAULT_LINK_TARGET = " target=\"_blank\""; @PrimaryKey @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) private Long id; @Persistent private Date timestamp; // Enum for the classification of this content such as "Fact", "Context", "Analysis", etc. @Persistent private ContentItemType contentItemType; // This is the HTML content. @Persistent private String content; @Persistent private Importance importance = Importance.MEDIUM; @Persistent private Long livingStoryId; @Persistent private PublishState publishState = PublishState.DRAFT; @Persistent private Set<Long> contributorIds; @Persistent private Set<Long> linkedContentEntityIds; @Persistent private Set<Long> themeIds; @Persistent @Embedded private LocationEntity location; @PersistenceCapable @EmbeddedOnly public static class LocationEntity implements Serializable, JSONSerializable { // The appengine datastore actually supports a core value type of 'GeoPt' that consists of a // lat, long. But unfortunately, this has not been implemented in the Java API. So for now, // we'll just have to store them separately ourselves. @Persistent private Double latitude; @Persistent private Double longitude; @Persistent private String description; LocationEntity(Double latitude, Double longitude, String description) { this.latitude = latitude; this.longitude = longitude; this.description = description; } public Double getLatitude() { return latitude; } public Double getLongitude() { return longitude; } public String getDescription() { return description; } public void setLatitude(Double latitude) { this.latitude = latitude; } public void setLongitude(Double longitude) { this.longitude = longitude; } public void setDescription(String description) { this.description = description; } public Location toClientObject() { return new Location(latitude, longitude, description); } @Override public JSONObject toJSON() { JSONObject object = new JSONObject(); try { object.put("latitude", latitude); object.put("longitude", longitude); object.put("description", description); } catch (JSONException ex) { throw new RuntimeException(ex); } return object; } public static LocationEntity fromJSON(JSONObject json) { try { return new LocationEntity(json.has("latitude") ? json.getDouble("latitude") : null, json.has("longitude") ? json.getDouble("longitude") : null, json.getString("description")); } catch (JSONException ex) { throw new RuntimeException(ex); } } } /*** Fields related to the source ***/ @Persistent private String sourceDescription; @Persistent private Long sourceContentEntityId; /*** Event specific properties ***/ @Persistent private Date startDate; @Persistent private Date endDate; @Persistent private String eventUpdate; @Persistent private String eventSummary; /*** Property shared by Background and Player types ***/ @Persistent private String name; /*** Player specific properties ***/ @Persistent private List<String> aliases; @Persistent private PlayerType playerType; @Persistent private Long photoContentEntityId; // id of an associated asset entity /*** StoryPlayer specific properties ***/ @Persistent private Long parentPlayerContentEntityId; /*** Asset specific properties ***/ @Persistent private AssetType assetType; @Persistent private String caption; @Persistent private String previewUrl; /*** Narrative specific properties ***/ @Persistent private String headline; @Persistent private NarrativeType narrativeType; @Persistent private Boolean isStandalone = true; @Persistent private Date narrativeDate; @Persistent private String narrativeSummary; private BaseContentEntity() { } public BaseContentEntity(Date timestamp, ContentItemType contentItemType, String content, Importance importance, Long livingStoryId) { this.timestamp = timestamp; this.contentItemType = contentItemType; this.content = content; this.importance = importance; this.livingStoryId = livingStoryId; } public Long getId() { return id; } public Date getTimestamp() { return timestamp; } public void setTimestamp(Date timestamp) { this.timestamp = timestamp; } public ContentItemType getContentItemType() { return contentItemType; } public void setContentItemType(ContentItemType contentItemType) { this.contentItemType = contentItemType; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Importance getImportance() { return importance; } public void setImportance(Importance importance) { this.importance = importance; } @Override public Long getLivingStoryId() { return livingStoryId; } public void setLivingStoryId(Long livingStoryId) { this.livingStoryId = livingStoryId; } public PublishState getPublishState() { return publishState; } public void setPublishState(PublishState publishState) { this.publishState = publishState; } public Set<Long> getContributorIds() { return GlobalUtil.copySet(contributorIds); } public void setContributorIds(Set<Long> contributorIds) { this.contributorIds = GlobalUtil.copySet(contributorIds); } public void addContributorId(long contributorId) { if (contributorIds == null) { contributorIds = new HashSet<Long>(); } contributorIds.add(contributorId); } public void removeContributorId(long contributorId) { if (contributorIds != null) { contributorIds.remove(contributorId); } } public Set<Long> getLinkedContentEntityIds() { return GlobalUtil.copySet(linkedContentEntityIds); } public void setLinkedContentEntityIds(Set<Long> linkedContentEntityIds) { this.linkedContentEntityIds = GlobalUtil.copySet(linkedContentEntityIds); } public void addLinkedContentEntityId(long linkedContentEntityId) { if (linkedContentEntityIds == null) { linkedContentEntityIds = new HashSet<Long>(); } linkedContentEntityIds.add(linkedContentEntityId); } public void removeLinkedContentEntityId(long linkedContentEntityId) { if (linkedContentEntityIds != null) { linkedContentEntityIds.remove(linkedContentEntityId); } } public Set<Long> getThemeIds() { return GlobalUtil.copySet(themeIds); } public void setThemeIds(Set<Long> themeIds) { this.themeIds = GlobalUtil.copySet(themeIds); } public void removeThemeId(long themeId) { if (themeIds != null) { themeIds.remove(themeId); } } public Location getLocation() { return location.toClientObject(); } public void setLocation(Location location) { if (this.location == null) { this.location = new LocationEntity(location.getLatitude(), location.getLongitude(), location.getDescription()); } else { this.location.setLatitude(location.getLatitude()); this.location.setLongitude(location.getLongitude()); this.location.setDescription(location.getDescription()); } } private void setLocation(LocationEntity location) { this.location = location; } public String getSourceDescription() { return sourceDescription == null ? null : sourceDescription; } public void setSourceDescription(String sourceDescription) { if (sourceDescription != null) { this.sourceDescription = sourceDescription; } } public Long getSourceContentEntityId() { return sourceContentEntityId; } public void setSourceContentEntityId(Long sourceContentEntityId) { this.sourceContentEntityId = sourceContentEntityId; } public Date getEventStartDate() { return startDate; } public Date getEventEndDate() { return endDate; } public String getEventUpdate() { return eventUpdate; } public String getEventSummary() { return eventSummary; } public void setEventStartDate(Date eventStartDate) { this.startDate = eventStartDate; } public void setEventEndDate(Date eventEndDate) { this.endDate = eventEndDate; } public void setEventUpdate(String eventUpdate) { this.eventUpdate = eventUpdate; } public void setEventSummary(String eventSummary) { this.eventSummary = eventSummary; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<String> getAliases() { return aliases; } public void setAliases(List<String> aliases) { this.aliases = aliases; } public PlayerType getPlayerType() { return playerType; } public void setPlayerType(PlayerType playerType) { this.playerType = playerType; } public Long getPhotoContentEntityId() { return photoContentEntityId; } public void setPhotoContentEntityId(Long photoContentEntityId) { this.photoContentEntityId = photoContentEntityId; } public Long getParentPlayerContentEntityId() { return parentPlayerContentEntityId; } public void setParentPlayerContentEntityId(Long parentPlayerContentEntityId) { this.parentPlayerContentEntityId = parentPlayerContentEntityId; } public AssetType getAssetType() { return assetType; } public void setAssetType(AssetType assetType) { this.assetType = assetType; } public String getCaption() { return caption; } public void setCaption(String caption) { this.caption = caption; } public String getPreviewUrl() { return previewUrl; } public void setPreviewUrl(String previewUrl) { this.previewUrl = previewUrl; } public String getHeadline() { return headline; } public void setHeadline(String headline) { this.headline = headline; } public NarrativeType getNarrativeType() { return narrativeType; } public void setNarrativeType(NarrativeType narrativeType) { this.narrativeType = narrativeType; } public boolean isStandalone() { return isStandalone; } public void setIsStandalone(boolean isStandalone) { this.isStandalone = isStandalone; } public Date getNarrativeDate() { return narrativeDate; } public void setNarrativeDate(Date narrativeDate) { this.narrativeDate = narrativeDate; } public String getNarrativeSummary() { return narrativeSummary == null ? null : narrativeSummary; } public void setNarrativeSummary(String narrativeSummary) { this.narrativeSummary = narrativeSummary; } public void copyFields(BaseContentItem clientContentItem) { setTimestamp(clientContentItem.getTimestamp()); setContentItemType(clientContentItem.getContentItemType()); setContent(fixLinks(trimWithBrs(clientContentItem.getContent()))); setImportance(clientContentItem.getImportance()); setLivingStoryId(clientContentItem.getLivingStoryId()); setThemeIds(clientContentItem.getThemeIds()); setContributorIds(clientContentItem.getContributorIds()); setLinkedContentEntityIds(clientContentItem.getLinkedContentItemIds()); setPublishState(clientContentItem.getPublishState()); setLocation(clientContentItem.getLocation()); setSourceDescription(clientContentItem.getSourceDescription()); setSourceContentEntityId(clientContentItem.getSourceContentItemId()); switch (clientContentItem.getContentItemType()) { case EVENT: EventContentItem eventContentItem = (EventContentItem) clientContentItem; setEventStartDate(eventContentItem.getEventStartDate()); setEventEndDate(eventContentItem.getEventEndDate()); setEventUpdate(fixLinks(trimWithBrs(eventContentItem.getEventUpdate()))); setEventSummary(fixLinks(trimWithBrs(eventContentItem.getEventSummary()))); break; case PLAYER: if (clientContentItem.getLivingStoryId() == null) { PlayerContentItem playerContentItem = (PlayerContentItem) clientContentItem; setName(playerContentItem.getName()); setAliases(playerContentItem.getAliases()); setPlayerType(playerContentItem.getPlayerType()); setPhotoContentEntityId(playerContentItem.getPhotoContentItemId()); } else { StoryPlayerContentItem storyPlayerContentItem = (StoryPlayerContentItem) clientContentItem; setParentPlayerContentEntityId(storyPlayerContentItem.getParentPlayerContentItem().getId()); } break; case ASSET: AssetContentItem assetContentItem = (AssetContentItem) clientContentItem; setAssetType(assetContentItem.getAssetType()); setCaption(assetContentItem.getCaption()); setPreviewUrl(assetContentItem.getPreviewUrl()); break; case NARRATIVE: NarrativeContentItem narrativeContentItem = (NarrativeContentItem) clientContentItem; setHeadline(narrativeContentItem.getHeadline()); setNarrativeType(narrativeContentItem.getNarrativeType()); setIsStandalone(narrativeContentItem.isStandalone()); setNarrativeDate(narrativeContentItem.getNarrativeDate()); setNarrativeSummary(narrativeContentItem.getNarrativeSummary()); break; case BACKGROUND: BackgroundContentItem backgroundContentItem = (BackgroundContentItem) clientContentItem; setName(backgroundContentItem.getConceptName()); break; } } /** * Returns a form of input that's trimmed, including the removal of any leading or trailing <br/>s * @input * @return trimmed input */ private static String trimWithBrs(String input) { return input.replaceFirst("^(\\s|<br/?>|<br></br>)+", "").replaceFirst("(\\s|<br/?>|<br></br>)+$", "") .trim(); } /** * Examines the content for anchor tags that look like they point to external pages * (in general, any link that doesn't start with 'javascript:', and adds a * target="_blank" attribute to them, if there isn't a target attribute already. * @param content Content to fix up. * @return The modified content string, with links fixed to pop up new windows. */ private String fixLinks(String content) { Matcher matcher = EXTERNAL_LINK_PATTERN.matcher(content); StringBuffer sb = new StringBuffer(); while (matcher.find()) { String link = matcher.group(0); if (!TARGET_ATTR_PATTERN.matcher(link).find()) { link = link.replace(">", DEFAULT_LINK_TARGET + ">"); } matcher.appendReplacement(sb, Matcher.quoteReplacement(link)); } matcher.appendTail(sb); return sb.toString(); } public BaseContentItem toClientObject() { BaseContentItem ret = toClientObjectImpl(); ret.setPublishState(publishState); ret.setThemeIds(themeIds); ret.setLinkedContentItemIds(linkedContentEntityIds); ret.setLocation(location.toClientObject()); ret.setTimeElapsedSinceLastUpdate(TimeUtil.getElapsedTimeString(this.timestamp)); BaseContentItem sourceContentItem = null; Long sourceContentEntityId = getSourceContentEntityId(); if (sourceContentEntityId != null) { try { sourceContentItem = new ContentRpcImpl().getContentItem(sourceContentEntityId, false); } catch (JDOException ex) { // leave sourceContentEntity as null; } } ret.setSourceContentItem(sourceContentItem); ret.setSourceDescription(getSourceDescription()); return ret; } private BaseContentItem toClientObjectImpl() { switch (getContentItemType()) { case EVENT: if (GlobalUtil.isContentEmpty(getEventUpdate())) { return new DefaultContentItem(getId(), getLivingStoryId()); } else { return new EventContentItem(getId(), getTimestamp(), getContributorIds(), getImportance(), getLivingStoryId(), getEventStartDate(), getEventEndDate(), getEventUpdate(), getEventSummary(), getContent()); } case PLAYER: Long livingStoryId = getLivingStoryId(); if (livingStoryId == null) { AssetContentItem photoContentItem = null; if (getPhotoContentEntityId() != null) { try { photoContentItem = (AssetContentItem) new ContentRpcImpl() .getContentItem(getPhotoContentEntityId(), false); } catch (JDOException ex) { // leave photoContentItem as null; } } return new PlayerContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getName(), getAliases(), getPlayerType(), photoContentItem); } else { return new StoryPlayerContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), livingStoryId, (PlayerContentItem) new ContentRpcImpl() .getContentItem(getParentPlayerContentEntityId(), false)); } case QUOTE: return new QuoteContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId()); case BACKGROUND: return new BackgroundContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId(), getName()); case DATA: return new DataContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId()); case ASSET: return new AssetContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId(), getAssetType(), getCaption(), getPreviewUrl()); case NARRATIVE: return new NarrativeContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId(), getHeadline(), getNarrativeType(), isStandalone(), getNarrativeDate(), getNarrativeSummary()); case REACTION: return new ReactionContentItem(getId(), getTimestamp(), getContributorIds(), getContent(), getImportance(), getLivingStoryId()); default: throw new IllegalStateException("Unknown Content Item Type"); } } public static BaseContentEntity fromClientObject(BaseContentItem clientContentItem) { BaseContentEntity contentEntity = new BaseContentEntity(); contentEntity.copyFields(clientContentItem); return contentEntity; } @Override public String toString() { try { return toJSON().toString(2); } catch (JSONException ex) { throw new RuntimeException(ex); } } @Override public JSONObject toJSON() { JSONObject object = new JSONObject(); try { object.put("id", getId()); object.put("timestamp", SimpleDateFormat.getInstance().format(getTimestamp())); object.put("contentItemType", getContentItemType().name()); object.put("content", getContent()); object.put("importance", getImportance().name()); object.put("livingStoryId", getLivingStoryId()); object.put("publishState", getPublishState().name()); object.put("contributorIds", new JSONArray(getContributorIds())); object.put("linkedContentEntityIds", new JSONArray(getLinkedContentEntityIds())); object.put("themeIds", new JSONArray(getThemeIds())); object.put("location", location.toJSON()); object.put("sourceDescription", getSourceDescription()); object.put("sourceContentEntityId", getSourceContentEntityId()); // Optional properties depending on contentItemType switch (getContentItemType()) { case EVENT: if (startDate != null) { object.put("startDate", SimpleDateFormat.getInstance().format(startDate)); } if (endDate != null) { object.put("endDate", SimpleDateFormat.getInstance().format(endDate)); } object.put("eventUpdate", getEventUpdate()); object.put("eventSummary", getEventSummary()); break; case PLAYER: if (getLivingStoryId() == null) { object.put("name", name); object.put("playerType", playerType.name()); if (aliases != null && !aliases.isEmpty()) { object.put("aliases", new JSONArray(getAliases())); } if (photoContentEntityId != null) { object.put("photoContentEntityId", photoContentEntityId); } } else { object.put("parentPlayerContentEntityId", parentPlayerContentEntityId); } break; case ASSET: object.put("assetType", assetType.name()); object.put("caption", getCaption()); object.put("previewUrl", getPreviewUrl()); break; case NARRATIVE: object.put("headline", getHeadline()); object.put("narrativeType", narrativeType.name()); object.put("isStandalone", isStandalone); if (narrativeDate != null) { object.put("narrativeDate", SimpleDateFormat.getInstance().format(narrativeDate)); } object.put("narrativeSummary", getNarrativeSummary()); break; case BACKGROUND: object.put("name", name); break; } } catch (JSONException ex) { throw new RuntimeException(ex); } return object; } public static BaseContentEntity fromJSON(JSONObject json) { DateFormat dateFormatter = SimpleDateFormat.getInstance(); try { ContentItemType contentItemType = ContentItemType.valueOf(json.getString("contentItemType")); Long livingStoryId = json.has("livingStoryId") ? json.getLong("livingStoryId") : null; BaseContentEntity entity = new BaseContentEntity(dateFormatter.parse(json.getString("timestamp")), contentItemType, json.getString("content"), Importance.valueOf(json.getString("importance")), livingStoryId); entity.setPublishState(PublishState.valueOf(json.getString("publishState"))); Set<Long> contributorIds = new HashSet<Long>(); JSONArray contributorIdsJSON = json.getJSONArray("contributorIds"); for (int i = 0; i < contributorIdsJSON.length(); i++) { contributorIds.add(contributorIdsJSON.getLong(i)); } entity.setContributorIds(contributorIds); Set<Long> linkedContentEntityIds = new HashSet<Long>(); JSONArray linkedContentEntityIdsJSON = json.getJSONArray("linkedContentEntityIds"); for (int i = 0; i < linkedContentEntityIdsJSON.length(); i++) { linkedContentEntityIds.add(linkedContentEntityIdsJSON.getLong(i)); } entity.setLinkedContentEntityIds(linkedContentEntityIds); Set<Long> themeIds = new HashSet<Long>(); JSONArray themeIdsJSON = json.getJSONArray("themeIds"); for (int i = 0; i < themeIdsJSON.length(); i++) { themeIds.add(themeIdsJSON.getLong(i)); } entity.setThemeIds(themeIds); entity.setLocation(LocationEntity.fromJSON(json.getJSONObject("location"))); if (json.has("sourceDescription")) { entity.setSourceDescription(json.getString("sourceDescription")); } if (json.has("sourceContentEntityId")) { entity.setSourceContentEntityId(json.getLong("sourceContentEntityId")); } // Optional properties depending on contentItemType switch (contentItemType) { case EVENT: if (json.has("startDate")) { entity.setEventStartDate(dateFormatter.parse(json.getString("startDate"))); } if (json.has("endDate")) { entity.setEventEndDate(dateFormatter.parse(json.getString("endDate"))); } entity.setEventUpdate(json.getString("eventUpdate")); entity.setEventSummary(json.getString("eventSummary")); break; case PLAYER: if (livingStoryId == null) { entity.setName(json.getString("name")); entity.setPlayerType(PlayerType.valueOf(json.getString("playerType"))); if (json.has("aliases")) { List<String> aliases = new ArrayList<String>(); JSONArray aliasesJSON = json.getJSONArray("aliases"); for (int i = 0; i < aliasesJSON.length(); i++) { aliases.add(aliasesJSON.getString(i)); } entity.setAliases(aliases); } if (json.has("photoContentEntityId")) { entity.setPhotoContentEntityId(json.getLong("photoContentEntityId")); } } else { entity.setParentPlayerContentEntityId(json.getLong("parentPlayerContentEntityId")); } break; case ASSET: entity.setAssetType(AssetType.valueOf(json.getString("assetType"))); entity.setCaption(json.getString("caption")); entity.setPreviewUrl(json.getString("previewUrl")); break; case NARRATIVE: entity.setHeadline(json.getString("headline")); entity.setNarrativeType(NarrativeType.valueOf(json.getString("narrativeType"))); // To convert exports that may have been done before the isStandalone field was // introduced, set the value to 'false' if the field is not present entity.setIsStandalone(json.has("isStandalone") ? json.getBoolean("isStandalone") : false); if (json.has("narrativeDate")) { entity.setNarrativeDate(dateFormatter.parse(json.getString("narrativeDate"))); } if (json.has("narrativeSummary")) { entity.setNarrativeSummary(json.getString("narrativeSummary")); } break; case BACKGROUND: if (json.has("name")) { entity.setName(json.getString("name")); } break; } return entity; } catch (JSONException ex) { throw new RuntimeException(ex); } catch (ParseException ex) { throw new RuntimeException(ex); } } }