55import java .io .StringWriter ;
66import java .util .ArrayList ;
77import java .util .List ;
8+ import java .util .Set ;
9+
10+ import org .maproulette .client .exception .MapRouletteRuntimeParseException ;
811
912import com .fasterxml .jackson .core .JsonFactory ;
1013import com .fasterxml .jackson .core .JsonGenerator ;
2326import lombok .Data ;
2427import lombok .NoArgsConstructor ;
2528import lombok .NonNull ;
29+ import lombok .Value ;
2630import lombok .extern .slf4j .Slf4j ;
2731
2832/**
3438@ AllArgsConstructor
3539@ JsonDeserialize (using = RuleList .RuleListDeserializer .class )
3640@ JsonSerialize (using = RuleList .RuleListSerializer .class )
41+ @ Value
3742@ Slf4j
3843public class RuleList implements Serializable
3944{
45+ public static final String CONDITION_AND = "AND" ;
46+ public static final String CONDITION_OR = "OR" ;
47+ private static final Set <String > VALID_CONDITIONS = Set .of (CONDITION_AND , CONDITION_OR );
48+
4049 private static final String KEY_CONDITION = "condition" ;
4150 private static final String KEY_RULES = "rules" ;
4251 private static final long serialVersionUID = -1085774480815117637L ;
@@ -55,11 +64,13 @@ public class RuleList implements Serializable
5564
5665 public boolean isSet ()
5766 {
58- // A condition is needed
59- if (this .condition == null || this .condition .isEmpty ())
67+ // The rule list is "set" if the condition is empty, and there are no rules, and there are
68+ // no nested priority rules
69+ if (this .condition .isEmpty () && this .rules .isEmpty () && this .ruleList .isEmpty ())
6070 {
6171 return false ;
6272 }
73+
6374 // It is possible to have an empty 'rules' and a non-empty nested rule list.
6475 // It is invalid for both to be empty at the same time.
6576 if (this .rules .isEmpty () && this .ruleList .isEmpty ())
@@ -90,22 +101,25 @@ private static void serializeRuleListHelper(final List<RuleList> ruleListList,
90101 {
91102 for (final RuleList ruleList : ruleListList )
92103 {
104+ final String condition = ruleList .getCondition ();
105+ // For nested rule lists (eg rule lists that have a parent), these REQUIRE the
106+ // condition to be non-empty and valid.
107+ if (!CONDITION_AND .equals (condition ) && !CONDITION_OR .equals (condition ))
108+ {
109+ throw new MapRouletteRuntimeParseException (
110+ String .format ("Condition '%s' is not known" , condition ));
111+ }
112+
93113 gen .writeStartObject ();
94- gen .writeStringField ("condition" , ruleList .getCondition ());
95- gen .writeArrayFieldStart ("rules" );
96- if ( ruleList .getRuleList () != null )
114+ gen .writeStringField (KEY_CONDITION , ruleList .getCondition ());
115+ gen .writeArrayFieldStart (KEY_RULES );
116+ for ( final RuleList nestedRuleList : ruleList .getRuleList ())
97117 {
98- for (final RuleList nestedRuleList : ruleList .getRuleList ())
99- {
100- serializeRuleListHelper (nestedRuleList .getRuleList (), gen );
101- }
118+ serializeRuleListHelper (nestedRuleList .getRuleList (), gen );
102119 }
103- if ( ruleList .getRules () != null )
120+ for ( final PriorityRule priorityRule : ruleList .getRules ())
104121 {
105- for (final PriorityRule priorityRule : ruleList .getRules ())
106- {
107- gen .writeObject (priorityRule );
108- }
122+ gen .writeObject (priorityRule );
109123 }
110124 gen .writeEndArray ();
111125 gen .writeEndObject ();
@@ -115,21 +129,24 @@ private static void serializeRuleListHelper(final List<RuleList> ruleListList,
115129 private void serializeRuleListAsObject (final RuleList value , final JsonGenerator gen ,
116130 final SerializerProvider serializers ) throws IOException
117131 {
118- gen .writeStartObject ();
119- gen .writeStringField ("condition" , value .getCondition ());
120- gen .writeArrayFieldStart ("rules" );
121- if (value .getRuleList () != null )
132+ final String condition = value .getCondition ();
133+ // A condition must be valid
134+ if (!VALID_CONDITIONS .contains (condition ))
122135 {
123- serializeRuleListHelper (value .getRuleList (), gen );
136+ throw new MapRouletteRuntimeParseException (
137+ String .format ("Condition '%s' is not known" , condition ));
124138 }
125139
126- if (value .getRules () != null )
140+ gen .writeStartObject ();
141+ gen .writeStringField (KEY_CONDITION , condition );
142+ gen .writeArrayFieldStart (KEY_RULES );
143+
144+ serializeRuleListHelper (value .getRuleList (), gen );
145+ for (final PriorityRule priorityRule : value .getRules ())
127146 {
128- for (final PriorityRule priorityRule : value .getRules ())
129- {
130- gen .writeObject (priorityRule );
131- }
147+ gen .writeObject (priorityRule );
132148 }
149+
133150 gen .writeEndArray ();
134151 gen .writeEndObject ();
135152 gen .flush ();
@@ -188,35 +205,58 @@ public RuleListDeserializer(final Class<?> valueClass)
188205 private static RuleList buildRuleListHelper (final JsonNode node ,
189206 final DeserializationContext ctxt )
190207 {
191- // When the serialized format lacks required values, just create an empty RuleList to
192- // avoid NPE.
193- if (node .get ("condition" ) == null )
208+ final JsonNode conditionNode = node .get (KEY_CONDITION );
209+ final JsonNode rulesNode = node .get (KEY_RULES );
210+
211+ // If NEITHER a condition nor rules is in the object, return a dummy RuleList which will
212+ // serialize to '{}'.
213+ if (conditionNode == null && rulesNode == null )
194214 {
195215 return RuleList .builder ().build ();
196216 }
197- final RuleList ret = RuleList .builder ().condition (node .get ("condition" ).asText ())
198- .ruleList (new ArrayList <>()).rules (new ArrayList <>()).build ();
199217
200- for (final JsonNode jsonNode : node .withArray ("rules" ))
218+ // Both the 'condition' and 'rules' must appear, otherwise it is an invalid object.
219+ if (conditionNode == null || rulesNode == null )
220+ {
221+ throw new MapRouletteRuntimeParseException (
222+ "parse error: the rulelist requires both a 'condition' and 'rules' field" );
223+ }
224+
225+ if (!VALID_CONDITIONS .contains (conditionNode .asText ()))
201226 {
202- if (jsonNode .get ("condition" ) != null )
227+ throw new MapRouletteRuntimeParseException (
228+ String .format ("Condition '%s' is not known" , conditionNode .asText ()));
229+ }
230+
231+ final RuleListBuilder ret = RuleList .builder ().condition (conditionNode .asText ())
232+ .ruleList (new ArrayList <>()).rules (new ArrayList <>());
233+
234+ for (final JsonNode jsonNode : node .withArray (KEY_RULES ))
235+ {
236+ if (jsonNode .get (KEY_CONDITION ) != null || jsonNode .get (KEY_RULES ) != null )
203237 {
204238 // If the child is a PriorityRule, do a recursive call to build the rule and add
205239 // it to the list.
206240 final RuleList child = buildRuleListHelper (jsonNode , ctxt );
207- ret .getRuleList () .add (child );
241+ ret .ruleList$value .add (child );
208242 }
209243 else
210244 {
245+ if (jsonNode .get ("type" ) == null || jsonNode .get ("operator" ) == null
246+ || jsonNode .get ("value" ) == null )
247+ {
248+ throw new MapRouletteRuntimeParseException (
249+ "Nested object is not a PriorityRule nor a RuleList! Parsing failed!" );
250+ }
211251 final PriorityRule priorityRule = PriorityRule .builder ()
212252 .type (jsonNode .get ("type" ).asText ())
213253 .operator (jsonNode .get ("operator" ).asText ())
214254 .value (jsonNode .get ("value" ).asText ()).build ();
215- ret .getRules () .add (priorityRule );
255+ ret .rules$value .add (priorityRule );
216256 }
217257 }
218258
219- return ret ;
259+ return ret . build () ;
220260 }
221261
222262 /**
0 commit comments