/**
   * TODO: Description.
   *
   * @param source
   * @param target
   * @param syncOperation the parent {@link ObjectMapping.SyncOperation} instance
   * @return TODO.
   * @throws SynchronizationException TODO.
   */
  public Action getAction(
      LazyObjectAccessor source,
      LazyObjectAccessor target,
      final ObjectMapping.SyncOperation syncOperation)
      throws SynchronizationException {
    Action result = null;
    if (action != null) { // static action specified
      result = action;
    } else if (script != null) { // action is dynamically determine
      Map<String, Object> scope = new HashMap<String, Object>();
      if (null != scriptScope) {
        // Make a thread safe copy and put the variables into the scope
        for (Map.Entry<String, Object> entry : Utils.deepCopy(scriptScope).entrySet()) {
          if (scope.containsKey(entry.getKey())) {
            continue;
          }
          scope.put(entry.getKey(), entry.getValue());
        }
      }
      Map<String, Object> recon = new HashMap<String, Object>();
      scope.put("recon", recon);
      JsonValue actionParam = null;
      if (syncOperation instanceof ObjectMapping.TargetSyncOperation) {
        actionParam = ((ObjectMapping.TargetSyncOperation) syncOperation).toJsonValue();
      } else if (syncOperation instanceof ObjectMapping.SourceSyncOperation) {
        actionParam = ((ObjectMapping.SourceSyncOperation) syncOperation).toJsonValue();
      }
      if (null != actionParam) {
        // FIXME Decide if leading underscore should be used here or not
        actionParam.put("_" + ActionRequest.FIELD_ACTION, "performAction");
        recon.put("actionParam", actionParam.getObject());
      }

      scope.put("sourceAction", (syncOperation instanceof ObjectMapping.SourceSyncOperation));
      if (source != null) {
        scope.put("source", source.asMap());
      }
      if (target != null) {
        scope.put("target", target.asMap());
      }
      try {
        result = Enum.valueOf(Action.class, script.exec(scope).toString());
      } catch (NullPointerException npe) {
        throw new SynchronizationException("action script returned null value");
      } catch (IllegalArgumentException iae) {
        throw new SynchronizationException("action script returned invalid action");
      } catch (ScriptException se) {
        LOGGER.debug("action script encountered exception", se);
        throw new SynchronizationException(se);
      }
    }
    return result;
  }
 /**
  * Returns the exception in a JSON object structure, suitable for inclusion in the entity of an
  * HTTP error response. The JSON representation looks like this:
  *
  * <pre>
  * {
  *     "code"    : 404,
  *     "reason"  : "...",  // optional
  *     "message" : "...",  // required
  *     "detail"  : { ... } // optional
  *     "cause"   : { ... } // optional iff includeCause is set to true
  * }
  * </pre>
  *
  * @return The exception in a JSON object structure, suitable for inclusion in the entity of an
  *     HTTP error response.
  */
 public final JsonValue toJsonValue() {
   final Map<String, Object> result = new LinkedHashMap<String, Object>(4);
   result.put(FIELD_CODE, code); // required
   if (reason != null) { // optional
     result.put(FIELD_REASON, reason);
   }
   final String message = getMessage();
   if (message != null) { // should always be present
     result.put(FIELD_MESSAGE, message);
   }
   if (!detail.isNull()) {
     result.put(FIELD_DETAIL, detail.getObject());
   }
   if (includeCause && getCause() != null && getCause().getMessage() != null) {
     final Map<String, Object> cause = new LinkedHashMap<String, Object>(2);
     cause.put("message", getCause().getMessage());
     result.put(FIELD_CAUSE, cause);
   }
   return new JsonValue(result);
 }
 /**
  * Returns a JSON object containing only the specified fields from the provided JSON value. If the
  * list of fields is empty then the value is returned unchanged.
  *
  * <p><b>NOTE:</b> this method only performs a shallow copy of extracted fields, so changes to the
  * filtered JSON value may impact the original JSON value, and vice-versa.
  *
  * @param resource The JSON value whose fields are to be filtered.
  * @param fields The list of fields to be extracted.
  * @return The filtered JSON value.
  */
 public static JsonValue filterResource(
     final JsonValue resource, final Collection<JsonPointer> fields) {
   if (fields.isEmpty() || resource.isNull() || resource.size() == 0) {
     return resource;
   } else {
     final Map<String, Object> filtered = new LinkedHashMap<String, Object>(fields.size());
     for (JsonPointer field : fields) {
       if (field.isEmpty()) {
         // Special case - copy resource fields (assumes Map).
         filtered.putAll(resource.asMap());
       } else {
         // FIXME: what should we do if the field refers to an array element?
         final JsonValue value = resource.get(field);
         if (value != null) {
           final String key = field.leaf();
           filtered.put(key, value.getObject());
         }
       }
     }
     return new JsonValue(filtered);
   }
 }
 @Test(dataProvider = "testFilterData")
 public void testFilter(List<JsonPointer> filter, JsonValue content, JsonValue expected) {
   Assertions.assertThat(Resources.filterResource(content, filter).getObject())
       .isEqualTo(expected.getObject());
 }