Skip to content

Commit

Permalink
Add support for SnakeYaml to parse timestamps correctly when timezone…
Browse files Browse the repository at this point in the history
… is set or not (#5626)

* - Create a new SnakeYaml construct for timestamp to be able to handle timezones.
- Tests added.

* - Create a new SnakeYaml construct for timestamp to be able to handle timezones.
- Tests added.

* Update CustomConstructYamlTimeStamp to consistently return a given date as a String.
  • Loading branch information
MalloD12 committed Mar 12, 2024
1 parent 7f6ffc5 commit 74183f5
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package liquibase.parser.core.yaml;

import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.ScalarNode;

import java.util.Calendar;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CustomConstructYamlTimestamp extends SafeConstructor.ConstructYamlTimestamp {
private final static Pattern TIMESTAMP_REGEXP = Pattern.compile(
"^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:(?:[Tt]|[ \t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \t]*(?:Z|([-+][0-9][0-9]?)(?::([0-9][0-9])?)?))?)?$");
private final static Pattern YMD_REGEXP =
Pattern.compile("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)$");

private Calendar calendar;

@Override
public Object construct(Node node) {
ScalarNode scalar = (ScalarNode) node;
String nodeValue = scalar.getValue();
Matcher match = YMD_REGEXP.matcher(nodeValue);
if (match.matches()) {
String year_s = match.group(1);
String month_s = match.group(2);
String day_s = match.group(3);
calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.clear();
calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
// Java's months are zero-based...
calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1); // x
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
return calendar.getTime();
} else {
match = TIMESTAMP_REGEXP.matcher(nodeValue);
if (!match.matches()) {
throw new YAMLException("Unexpected timestamp: " + nodeValue);
}
String year_s = match.group(1);
String month_s = match.group(2);
String day_s = match.group(3);
String hour_s = match.group(4);
String min_s = match.group(5);
// seconds and milliseconds
String seconds = match.group(6);
String millis = match.group(7);
if (millis != null) {
seconds = seconds + "." + millis;
}
double fractions = Double.parseDouble(seconds);
int sec_s = (int) Math.round(Math.floor(fractions));
int usec = (int) Math.round((fractions - sec_s) * 1000);
// timezone
String timezoneh_s = match.group(8);
String timezonem_s = match.group(9);
TimeZone timeZone = null;
if (timezoneh_s != null) {
String time = timezonem_s != null ? ":" + timezonem_s : "00";
timeZone = TimeZone.getTimeZone("GMT" + timezoneh_s + time);
calendar = Calendar.getInstance(timeZone);
;
} else {
calendar = Calendar.getInstance();
}
calendar.set(Calendar.YEAR, Integer.parseInt(year_s));
// Java's months are zero-based...
calendar.set(Calendar.MONTH, Integer.parseInt(month_s) - 1);
calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(day_s));
calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour_s));
calendar.set(Calendar.MINUTE, Integer.parseInt(min_s));
calendar.set(Calendar.SECOND, sec_s);
calendar.set(Calendar.MILLISECOND, usec);
if (timeZone == null) {
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime());
} else {
return calendar.getTime().toString();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import liquibase.resource.Resource;
import liquibase.resource.ResourceAccessor;
import liquibase.util.FileUtil;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.nodes.Tag;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -25,7 +27,7 @@ public class YamlChangeLogParser extends YamlParser implements ChangeLogParser {

@Override
public DatabaseChangeLog parse(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters, ResourceAccessor resourceAccessor) throws ChangeLogParseException {
Yaml yaml = new Yaml(new SafeConstructor(createLoaderOptions()));
Yaml yaml = new Yaml(new CustomSafeConstructor(createLoaderOptions()));

try {
Resource changelog = resourceAccessor.get(physicalChangeLogLocation);
Expand Down Expand Up @@ -183,4 +185,16 @@ protected void replaceParameters(Object obj, ChangeLogParameters changeLogParame
}
}
}

static class CustomSafeConstructor extends SafeConstructor {
/**
* Create an instance
*
* @param loaderOptions - the configuration options
*/
public CustomSafeConstructor(LoaderOptions loaderOptions) {
super(loaderOptions);
this.yamlConstructors.put(Tag.TIMESTAMP, new CustomConstructYamlTimestamp());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package liquibase.parser.core.yaml

import org.yaml.snakeyaml.DumperOptions
import org.yaml.snakeyaml.nodes.ScalarNode
import org.yaml.snakeyaml.nodes.Tag
import spock.lang.Specification

class CustomConstructYamlTimestampTest extends Specification {
def "validate timestamp is parsed correctly without specifying a timezone"() {
given:
def node = new ScalarNode(new Tag("tag:yaml.org,2002:timestamp"), "2018-03-09 08:41:31.000", null, null, DumperOptions.ScalarStyle.createStyle(new Character('\'' as char)))
def customConstructYamlTimestamp = new CustomConstructYamlTimestamp()
when:
def returnedValue = customConstructYamlTimestamp.construct(node)

then:
returnedValue.toString() == "2018-03-09 08:41:31"
}

def "validate timestamp is parsed correctly specifying a timezone"() {
given:
TimeZone.setDefault(TimeZone.getTimeZone("PST"))
def node = new ScalarNode(new Tag("tag:yaml.org,2002:timestamp"), "2018-03-09 08:41:31.000+08:00", null, null, DumperOptions.ScalarStyle.createStyle(new Character('\'' as char)))
def customConstructYamlTimestamp = new CustomConstructYamlTimestamp()
when:
def returnedValue = customConstructYamlTimestamp.construct(node)

then:
returnedValue.toString() == "Thu Mar 08 16:41:31 PST 2018"
}
}

0 comments on commit 74183f5

Please sign in to comment.