/* * Copyright 2015 The Project Buendia Authors * * 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 distrib- * uted 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 * specific language governing permissions and limitations under the License. */ package org.openmrs.projectbuendia.webservices.rest; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.openmrs.module.webservices.rest.SimpleObject; import org.openmrs.projectbuendia.Utils; import org.projectbuendia.openmrs.api.SyncToken; import javax.annotation.Nullable; import java.io.IOException; import java.text.ParseException; import java.util.Date; /** * Utilities for working with {@link SyncToken}s in HTTP requests and responses. */ public class SyncTokenUtils { private static final String JSON_FIELD_TIMESTAMP = "t"; private static final String JSON_FIELD_UUID = "u"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** Creates a {@link SyncToken} from its corresponding JSON representation. */ public static SyncToken jsonToSyncToken(String param) throws ParseException, JsonParseException, JsonMappingException { SimpleObject object; try { object = SimpleObject.parseJson(param); } catch (JsonParseException | JsonMappingException e) { // These are subclasses of IOException, so we need to specifically add a catch clause // that rethrows. throw e; } catch (IOException e) { // Should never occur, we're reading from a string. throw new RuntimeException(e); } if (object.get(JSON_FIELD_TIMESTAMP) == null) { throw new JsonMappingException("Didn't find a valid timestamp field in the sync token"); } return new SyncToken( Utils.fromIso8601(object.get(JSON_FIELD_TIMESTAMP).toString()), // This could be null, cast instead of calling toString(). (String) object.get(JSON_FIELD_UUID)); } /** Converts a {@link SyncToken} into a corresponding JSON representation. */ public static String syncTokenToJson(SyncToken token) { Object object = new SimpleObject() .add(JSON_FIELD_TIMESTAMP, Utils.toIso8601(token.greaterThanOrEqualToTimestamp)) .add(JSON_FIELD_UUID, token.greaterThanUuid); try { return OBJECT_MAPPER.writeValueAsString(object); } catch (IOException e) { // Shouldn't occur. throw new RuntimeException(e); } } private static final long REQUEST_BUFFER_WINDOW = 2000; /** * {@link SyncToken}s from the DAO aren't necessarily directly usable by the client - if the * most recently modified record is close enough to the current time, there's a chance that * records inserted by concurrent requests will never be synchronized. This method clamps a * DAO-provided SyncToken to a few seconds before the request time, to ensure that this edge * case can't occur. Note that in some circumstances, this could mean that a record is fetched * multiple times, which is ok under our synchronisation model. */ public static SyncToken clampSyncTokenToBufferedRequestTime( @Nullable SyncToken token, Date requestTime) { if (requestTime == null) { throw new IllegalArgumentException("requestTime cannot be null"); } Date earliestAllowableTime = new Date(requestTime.getTime() - REQUEST_BUFFER_WINDOW); if (token == null || earliestAllowableTime.before(token.greaterThanOrEqualToTimestamp)) { return new SyncToken(earliestAllowableTime, null); } return token; } }