Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

Commit

Permalink
Begins to address comments from 4157041. Tests still need to be updated.
Browse files Browse the repository at this point in the history
  • Loading branch information
Steve Hakusa committed Feb 9, 2011
1 parent 2497410 commit 4c89bb7
Show file tree
Hide file tree
Showing 16 changed files with 954 additions and 560 deletions.
4 changes: 2 additions & 2 deletions build.xml
Expand Up @@ -40,7 +40,7 @@
</javac>
<copy toDir="${build.java}/com/google/publicalerts/cap/schema"
failonerror="true">
<fileset dir="java/com/google/publicalerts/cap/schema"/>
<fileset dir="schema"/>
</copy>
</target>

Expand Down Expand Up @@ -80,7 +80,7 @@
</javac>
<copy toDir="${build.javatests}/com/google/publicalerts/cap/schema"
failonerror="true">
<fileset dir="java/com/google/publicalerts/cap/schema"/>
<fileset dir="schema"/>
</copy>
</target>

Expand Down
5 changes: 1 addition & 4 deletions java/com/google/publicalerts/cap/CapException.java
Expand Up @@ -29,7 +29,7 @@
* @author shakusa@google.com (Steve Hakusa)
*/
public class CapException extends Exception {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = -8060028892021203660L;

/** List of reasons for the exception */
private final List<Reason> reasons;
Expand Down Expand Up @@ -264,9 +264,6 @@ public enum Type implements ReasonType {
SENDER_IS_REQUIRED("<sender> is required"),
SENT_IS_REQUIRED("<sent> is required"),
STATUS_IS_REQUIRED("<status> is required"),
XMLNS_IS_REQUIRED("<alert> tag must include the xmlns attribute " +
"referencing the CAP URL as the namespace, e.g."
+ "xmlns:cap=\"urn:oasis:names:tc:emergency:cap:1.2\""),
UNSUPPORTED_ELEMENT("Unsupported element <{0}>"),
UNSUPPORTED_VALUE("Unsupported value <{0}> = \"{1}\""),
;
Expand Down
40 changes: 21 additions & 19 deletions java/com/google/publicalerts/cap/CapUtil.java
Expand Up @@ -38,27 +38,27 @@
*/
public class CapUtil {

// From the CAP spec:
//
// The date and time is represented in [dateTime] format
// (e. g., "2002-05-24T16:49:00-07:00" for 24 May 2002 at
// 16: 49 PDT). Alphabetic timezone designators such as "Z"
// MUST NOT be used. The timezone for UTC MUST be represented
// as "-00:00" or "+00:00"
//
// DATE_FORMATS format below do most of the work of validating this format,
// but do not support handling the colon in the time zone format
// and are more lenient in parsing months and dates than the spec specifies.
//
// TODO(andriy): We handle fractional seconds (hundredths or thousands), but
// because [dateTime] allows an arbitrary number of decimals, it would be
// better to devise a more robust and flexible solution here.
/**
* From the CAP spec:
*
* The date and time is represented in [dateTime] format
* (e. g., "2002-05-24T16:49:00-07:00" for 24 May 2002 at
* 16: 49 PDT). Alphabetic timezone designators such as "Z"
* MUST NOT be used. The timezone for UTC MUST be represented
* as "-00:00" or "+00:00"
*
* {@link #DATE_FORMATS} below do most of the work of validating this format,
* but do not support handling the colon in the time zone format
* and are more lenient in parsing months and dates than the spec specifies.
*/
// TODO (andriy): We handle fractional seconds (hundredths or thousands), but
// because [dateTime] allows an arbitrary number of decimals, it would be better
// to devise a more robust and flexible solution here.
private static final Pattern DATE_PATTERN = Pattern.compile(
"[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]" +
"(\\.[0-9]{2}([0-9])?)?[\\+|-][01][0-9]:[0-5][0-9]");
"[0-9]{4}-[01][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9](\\.[0-9]{2}([0-9])?)?"
+ "[\\+|-][01][0-9]:[0-5][0-9]");

// These are the (only) supported date formats; they must be covered by
// DATE_PATTERN.
// These are the (only) supported date formats; they must be covered by DATE_PATTERN.
private static final String [] DATE_FORMATS = {"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ss.SSZ",
"yyyy-MM-dd'T'HH:mm:ss.SSSZ"};
Expand All @@ -79,6 +79,8 @@ public static String getEnumValue(EnumValueDescriptor evd) {
if (value.endsWith(suffixWorkaround)) {
return value.substring(0, value.length() - suffixWorkaround.length());
} else if ("VeryLikely".equals(value)) {
// Special-case for the deprecated CAP 1.0 enum value for <certainty>
// The proto enum does not support a space, but the CAP XML enum has one.
return "Very Likely";
}
return value;
Expand Down
200 changes: 15 additions & 185 deletions java/com/google/publicalerts/cap/CapValidator.java
Expand Up @@ -16,28 +16,26 @@

package com.google.publicalerts.cap;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import com.google.protobuf.MessageOrBuilder;
import com.google.publicalerts.cap.CapException.Reason;
import com.google.publicalerts.cap.CapException.Type;

/**
* Validates a CAP alert.
* Supports CAP 1.0, 1.1, and 1.2.
* Validates portions of the CAP spec that are not possible to
* express in an XML schema definition file.
*
* @author shakusa@google.com (Steve Hakusa)
*/
public class CapValidator {
class CapValidator {

public static final String CAP10_XMLNS = "http://www.incident.com/cap/1.0";
public static final String CAP10_XMLNS =
"http://www.incident.com/cap/1.0";
public static final String CAP11_XMLNS =
"urn:oasis:names:tc:emergency:cap:1.1";
public static final String CAP12_XMLNS =
Expand All @@ -51,11 +49,6 @@ public class CapValidator {
CAP_XML_NAMESPACES.add(CAP12_XMLNS);
}

// Spec says the identifer and sender
// "MUST NOT include spaces, commas or restricted characters (< and &)"
private static final Pattern ID_SENDER_ILLEGAL_CHARS =
Pattern.compile(".*[\\s,&<].*");

/**
* Validates the given message.
*
Expand All @@ -77,17 +70,15 @@ public List<Reason> validate(MessageOrBuilder message, String xmlns,
return validateArea(
(AreaOrBuilder) message, infoSeqNum, version, visitChildren);
} else if (message instanceof ResourceOrBuilder) {
return validateResource((ResourceOrBuilder) message, infoSeqNum, version);
} else if (message instanceof ValuePairOrBuilder) {
return Collections.<Reason>emptyList();
} else if (message instanceof GroupOrBuilder) {
return Collections.<Reason>emptyList();
} else if (message instanceof CircleOrBuilder) {
return validateCircle((CircleOrBuilder) message, infoSeqNum);
return validateResource(
(ResourceOrBuilder) message, infoSeqNum, version);
} else if (message instanceof PolygonOrBuilder) {
return validatePolygon((PolygonOrBuilder) message, infoSeqNum);
} else if (message instanceof PointOrBuilder) {
return validatePoint((PointOrBuilder) message, infoSeqNum);
} else if (message instanceof ValuePairOrBuilder
|| message instanceof GroupOrBuilder
|| message instanceof CircleOrBuilder
|| message instanceof PointOrBuilder) {
return Collections.emptyList();
}

throw new IllegalArgumentException("Unsupported message: " + message);
Expand All @@ -109,51 +100,8 @@ public void validateAlert(AlertOrBuilder alert) throws CapException {
@SuppressWarnings("deprecation")
private List<Reason> validateAlert(
AlertOrBuilder alert, boolean visitChildren) {
int version = getValidateVersion(alert.getXmlns());

List<Reason> reasons = new ArrayList<Reason>();

if (!alert.hasXmlns()) {
reasons.add(new Reason(Type.XMLNS_IS_REQUIRED));
}

if (!alert.hasIdentifier() || "".equals(alert.getIdentifier())) {
reasons.add(
new Reason(Type.IDENTIFIER_IS_REQUIRED, alert.getIdentifier()));
} else if (ID_SENDER_ILLEGAL_CHARS.matcher(
alert.getIdentifier()).matches()) {
reasons.add(new Reason(Type.INVALID_IDENTIFIER));
}

if (alert.hasPassword() && version > 10) {
reasons.add(new Reason(Type.PASSWORD_DEPRECATED));
}

if (!alert.hasSender() || "".equals(alert.getSender())) {
reasons.add(new Reason(Type.SENDER_IS_REQUIRED));
} else if (ID_SENDER_ILLEGAL_CHARS.matcher(alert.getSender()).matches()) {
reasons.add(new Reason(Type.INVALID_SENDER, alert.getSender()));
}

if (!alert.hasSent() || "".equals(alert.getSent())) {
reasons.add(new Reason(Type.SENT_IS_REQUIRED));
} else if (!CapUtil.isValidDate(alert.getSent())) {
reasons.add(new Reason(Type.INVALID_SENT, alert.getSent()));
}

if (!alert.hasStatus()) {
reasons.add(new Reason(Type.STATUS_IS_REQUIRED));
}

if (!alert.hasMsgType()) {
reasons.add(new Reason(Type.MSGTYPE_IS_REQUIRED));
}

// Scope made required in CAP 1.1
if (!alert.hasScope() && !CAP10_XMLNS.equals(alert.getXmlns())) {
reasons.add(new Reason(Type.SCOPE_IS_REQUIRED));
}


if (alert.hasRestriction() && !"".equals(alert.getRestriction())
&& alert.getScope() != Alert.Scope.Restricted) {
reasons.add(new Reason(Type.RESTRICTION_SCOPE_MISMATCH));
Expand All @@ -165,6 +113,7 @@ private List<Reason> validateAlert(
}

if (visitChildren) {
int version = getValidateVersion(alert.getXmlns());
for (int i = 0; i < alert.getInfoOrBuilderList().size(); ++i) {
reasons.addAll(validateInfo(
alert.getInfoOrBuilderList().get(i), i, version, visitChildren));
Expand All @@ -178,53 +127,6 @@ List<Reason> validateInfo(InfoOrBuilder info, int infoSeqNum,
int version, boolean visitChildren) {
List<Reason> reasons = new ArrayList<Reason>();

// TODO(shakusa) Validate language is valid RFC 3066?

// Category made required in CAP 1.1
if (info.getCategoryCount() == 0 && version >= 11) {
reasons.add(new Reason(Type.INFO_CATEGORY_IS_REQUIRED, infoSeqNum));
}

if (!info.hasEvent()) {
reasons.add(new Reason(Type.INFO_EVENT_IS_REQUIRED, infoSeqNum));
}

if (!info.hasUrgency()) {
reasons.add(new Reason(Type.INFO_URGENCY_IS_REQUIRED, infoSeqNum));
}

if (!info.hasSeverity()) {
reasons.add(new Reason(Type.INFO_SEVERITY_IS_REQUIRED, infoSeqNum));
}

if (!info.hasCertainty()) {
reasons.add(new Reason(Type.INFO_CERTAINTY_IS_REQUIRED, infoSeqNum));
}
if (info.getCertainty() == Info.Certainty.VeryLikely && version > 10) {
reasons.add(new Reason(
Type.INFO_CERTAINTY_VERY_LIKELY_DEPRECATED, infoSeqNum));
}

if (info.hasEffective() && !CapUtil.isValidDate(info.getEffective())) {
reasons.add(new Reason(
Type.INFO_INVALID_EFFECTIVE, infoSeqNum, info.getEffective()));
}

if (info.hasOnset() && !CapUtil.isValidDate(info.getOnset())) {
reasons.add(new Reason(
Type.INFO_INVALID_ONSET, infoSeqNum, info.getOnset()));
}

if (info.hasExpires() && !CapUtil.isValidDate(info.getExpires())) {
reasons.add(new Reason(
Type.INFO_INVALID_EXPIRES, infoSeqNum, info.getExpires()));
}

if (info.hasWeb() && !isAbsoluteUri(info.getWeb())) {
reasons.add(new Reason(
Type.INFO_INVALID_WEB, infoSeqNum, info.getWeb()));
}

if (visitChildren) {
for (int i = 0; i < info.getAreaOrBuilderList().size(); ++i) {
reasons.addAll(validateArea(info.getAreaOrBuilderList().get(i),
Expand All @@ -243,18 +145,10 @@ List<Reason> validateArea(AreaOrBuilder area, int infoSeqNum,
int version, boolean visitChildren) {
List<Reason> reasons = new ArrayList<Reason>();

if (!area.hasAreaDesc()) {
reasons.add(new Reason(Type.AREA_AREA_DESC_IS_REQUIRED, infoSeqNum));
}

for (PolygonOrBuilder polygon : area.getPolygonOrBuilderList()) {
reasons.addAll(validatePolygon(polygon, infoSeqNum));
}

for (CircleOrBuilder circle : area.getCircleOrBuilderList()) {
reasons.addAll(validateCircle(circle, infoSeqNum));
}

if (area.hasCeiling() && !area.hasAltitude()) {
reasons.add(new Reason(Type.AREA_INVALID_CEILING, infoSeqNum));
}
Expand All @@ -269,78 +163,22 @@ List<Reason> validateArea(AreaOrBuilder area, int infoSeqNum,
return reasons;
}

List<Reason> validateCircle(CircleOrBuilder circle, int infoSeqNum) {
List<Reason> reasons = new ArrayList<Reason>();
reasons.addAll(validatePoint(circle.getPoint(), infoSeqNum));

if (circle.getRadius() < 0) {
reasons.add(new Reason(Type.AREA_INVALID_CIRCLE_RADIUS, infoSeqNum,
circle.getRadius()));
}
return reasons;
}

List<Reason> validatePolygon(PolygonOrBuilder polygon, int infoSeqNum) {
List<Reason> reasons = new ArrayList<Reason>();
if (polygon.getPointCount() < 4) {
reasons.add(new Reason(Type.AREA_INVALID_POLYGON_NUM_POINTS, infoSeqNum));
}

if (!polygon.getPoint(0).equals(
polygon.getPoint(polygon.getPointCount() - 1))) {
reasons.add(new Reason(Type.AREA_INVALID_POLYGON_START_END, infoSeqNum));
}

for (PointOrBuilder point : polygon.getPointOrBuilderList()) {
reasons.addAll(validatePoint(point, infoSeqNum));
}
return reasons;
}

List<Reason> validatePoint(PointOrBuilder point, int infoSeqNum) {
List<Reason> reasons = new ArrayList<Reason>();

if (point.getLatitude() < -90 || point.getLatitude() > 90) {
reasons.add(new Reason(Type.AREA_INVALID_POINT_LATITUDE, infoSeqNum,
point.getLatitude()));
}

if (point.getLongitude() < -180 || point.getLongitude() > 180) {
reasons.add(new Reason(Type.AREA_INVALID_POINT_LONGITUDE, infoSeqNum,
point.getLongitude()));
}

return reasons;
}

List<Reason> validateResource(
ResourceOrBuilder resource, int infoSeqNum, int version) {
List<Reason> reasons = new ArrayList<Reason>();

if (!resource.hasResourceDesc() || "".equals(resource.getResourceDesc())) {
reasons.add(new Reason(
Type.RESOURCE_RESOURCE_DESC_IS_REQUIRED, infoSeqNum));
}

if (!resource.hasMimeType() && version >= 12) {
reasons.add(new Reason(Type.RESOURCE_MIME_TYPE_IS_REQUIRED, infoSeqNum));
}

if (resource.hasSize() && resource.getSize() < 0) {
reasons.add(new Reason(
Type.RESOURCE_INVALID_SIZE, infoSeqNum, resource.getSize()));
}

// TODO(shakusa) Check if mime type is valid RFC 2046?

if (resource.hasUri() && !isAbsoluteUri(resource.getUri())) {
reasons.add(new Reason(
Type.RESOURCE_INVALID_URI, infoSeqNum, resource.getUri()));
}

// TODO(shakusa) Check if derefUri is base-64 encoded?

return reasons;
return Collections.<Reason>emptyList();
}

int getValidateVersion(String xmlns) {
Expand All @@ -351,12 +189,4 @@ int getValidateVersion(String xmlns) {
}
return 12;
}

boolean isAbsoluteUri(String uri) {
try {
return new URI(uri).isAbsolute();
} catch (URISyntaxException e) {
return false;
}
}
}

0 comments on commit 4c89bb7

Please sign in to comment.