Related
Is there a way to convert a string given in ISO 8601 period format to a format readable by humans and also translated to specified language?
Example:
P1W --en--> 1 Week
P2W --en--> 2 Weeks
P1W --pl--> 1 Tydzień
P2W --pl--> 2 Tygodnie
I am looking for an already implemented solution, without the necessity of defining periods translations.
The Unicode Common Locale Data Repository, commonly known as the CLDR, is an authoritative repository for localized information of all kinds, including translations.
Since Java 9, Java SE has used the CLDR for a lot of (if not all) localization functionality. But I am not aware of any localized formatting of java.time.Period instances using CLDR data files.
The CLDR has its own Java tools, but I experimented with the compiled .jar for hours and could not figure out how to use it. I couldn’t find documentation beyond the (very short) command line help. If it has an API for formatting periods, I would definitely favor using that.
But the CLDR data files are still the best place to get this information. (They can also be cloned from https://github.com/unicode-org/cldr) So, with that in mind, I wrote a (rather long) class to format Period instances using CLDR data files.
There are a few steps involved:
First, code must determine the language cardinality for a particular number. In English, there are only two choices: singular and plural. Other languages can be more complex. The common/supplemental/plurals.xml file in the CLDR contains all of this data for all locales; its format is documented on unicode.org.
Most localized data is in the common/main directory of the CLDR, as .xml files named by locale. The elements related to periods are in <ldml> → <units> → <unitLength type="long"> → <unit type="duration-week">, as well as type="duration-year", type="duration-month", and type="duration-day". Within the unit element are <unitPattern> elements, each with a count attribute referencing the cardinality described above.
Finally, the same locale-specific file in common/main contains <listPattern> elements which describe how to format a list of items, including <listPattern type="unit"> for assembling a list of period parts. The use of this data is described here.
Note that Period.parse accepts the ISO 8601 format, including weeks, but the Period class itself does not keep track of weeks, so if output with weeks is desired, code must divide the Period’s days by 7.
Here is what I came up with. The parsing is not robust and makes a lot of assumptions about correct CLDR data. There are probably ways to speed up the reading of the XML files, such as caching recently used ones, so consider this a proof of concept:
public class PeriodFormatter {
public enum Count {
EXACTLY0("0"),
EXACTLY1("1"),
ZERO,
ONE,
TWO,
FEW,
MANY,
OTHER;
private final String attributeValue;
Count() {
this.attributeValue = name().toLowerCase(Locale.US);
}
Count(String attributeValue) {
this.attributeValue = attributeValue;
}
String attributeValue() {
return attributeValue;
}
static Count forAttributeValue(String attrValue) {
for (Count count : values()) {
if (count.attributeValue.equals(attrValue)) {
return count;
}
}
throw new IllegalArgumentException(
"No Count with attribute value \"" + attrValue + "\"");
}
}
private static class Range {
final long start;
final long end;
Range(long value) {
this(value, value);
}
Range(long start,
long end) {
this.start = start;
this.end = end;
}
boolean contains(long value) {
return value >= start && value <= end;
}
boolean contains(double value) {
return value >= start && value <= end;
}
}
private enum Operand {
ABSOLUTE_VALUE("n"),
INTEGER_PART("i"),
FRACTIONAL_PRECISION("v"),
FRACTIONAL_PRECISION_TRIMMED("w"),
FRACTIONAL_PART("f"),
FRACTIONAL_PART_TRIMMED("t"),
EXPONENT_PART("c", "e");
final String[] tokens;
private Operand(String... tokens) {
this.tokens = tokens;
}
static Operand forToken(String token) {
for (Operand op : values()) {
if (Arrays.asList(op.tokens).contains(token)) {
return op;
}
}
throw new IllegalArgumentException(
"No Operand for token \"" + token + "\"");
}
}
private static class Expression {
final Operand operand;
final Long modValue;
Expression(Operand operand,
Long modValue) {
this.operand = operand;
this.modValue = modValue;
}
double evaluate(BigDecimal value) {
switch (operand) {
case ABSOLUTE_VALUE:
return Math.abs(value.doubleValue());
case INTEGER_PART:
return value.longValue();
case FRACTIONAL_PRECISION:
return Math.max(value.scale(), 0);
case FRACTIONAL_PRECISION_TRIMMED:
return Math.max(value.stripTrailingZeros().scale(), 0);
case FRACTIONAL_PART:
BigDecimal frac = value.remainder(BigDecimal.ONE);
frac = frac.movePointRight(Math.max(0, frac.scale()));
return frac.doubleValue();
case FRACTIONAL_PART_TRIMMED:
BigDecimal trimmed =
value.stripTrailingZeros().remainder(BigDecimal.ONE);
trimmed = trimmed.movePointRight(
Math.max(0, trimmed.scale()));
return trimmed.longValue();
case EXPONENT_PART:
String expStr = String.format("%e", value);
return Long.parseLong(
expStr.substring(expStr.indexOf('e') + 1));
default:
break;
}
throw new RuntimeException("Unknown operand " + operand);
}
}
private static abstract class Relation {
boolean negated;
Expression expr;
abstract boolean matches(BigDecimal value);
final boolean matchIfIntegral(BigDecimal value,
LongPredicate test) {
double evaluatedValue = expr.evaluate(value);
long rounded = Math.round(evaluatedValue);
return Math.abs(evaluatedValue - rounded) < 0.000001
&& test.test(rounded);
}
}
private static class IsRelation
extends Relation {
long value;
#Override
boolean matches(BigDecimal value) {
return matchIfIntegral(value, n -> n == this.value);
}
}
private static class InRelation
extends Relation {
final List<Range> ranges = new ArrayList<>();
#Override
boolean matches(BigDecimal value) {
return ranges.stream().anyMatch(
range -> matchIfIntegral(value, n -> range.contains(n)));
}
}
private static class WithinRelation
extends Relation {
final List<Range> ranges = new ArrayList<>();
#Override
boolean matches(BigDecimal value) {
return ranges.stream().anyMatch(r -> r.contains(value.longValue()));
}
}
private static class Condition {
final List<Relation> relations = new ArrayList<>();
boolean matches(BigDecimal value) {
return relations.stream().allMatch(r -> r.matches(value));
}
}
private static class AndConditionSequence {
final List<Condition> conditions = new ArrayList<>();
boolean matches(BigDecimal value) {
return conditions.stream().allMatch(c -> c.matches(value));
}
}
private static class PluralRule {
final Count count;
final List<AndConditionSequence> conditions = new ArrayList<>();
PluralRule(String countSpec,
String ruleSpec) {
this.count = Count.forAttributeValue(countSpec);
Scanner scanner = new Scanner(ruleSpec);
AndConditionSequence andSequence = new AndConditionSequence();
Condition condition = new Condition();
andSequence.conditions.add(condition);
this.conditions.add(andSequence);
while (true) {
String token = scanner.findWithinHorizon("\\S", 0);
if (token.equals("#")) {
// Ignore samples.
break;
}
Operand operand = Operand.forToken(token);
Long modValue = null;
token = scanner.findWithinHorizon(
"mod|%|is|in|within|!?=|not", 0);
if (token.equals("mod") || token.equals("%")) {
modValue = Long.valueOf(
scanner.findWithinHorizon("\\d+", 0));
token = scanner.findWithinHorizon(
"is|in|within|!?=|not", 0);
}
Relation relation;
boolean negated = false;
if (token.equals("not")) {
token = scanner.findWithinHorizon("in|within", 0);
if (token.equals("within")) {
WithinRelation within = new WithinRelation();
relation = within;
within.negated = true;
parseRanges(within.ranges, scanner);
} else {
InRelation in = new InRelation();
relation = in;
in.negated = true;
parseRanges(in.ranges, scanner);
}
relation.negated = true;
} else if (token.equals("is")) {
IsRelation is = new IsRelation();
relation = is;
token = scanner.findWithinHorizon("not|\\d+", 0);
if (token.equals("not")) {
is.negated = true;
token = scanner.findWithinHorizon("\\d+", 0);
}
is.value = Long.valueOf(token);
} else if (token.endsWith("=")) {
InRelation in = new InRelation();
relation = in;
in.negated = token.startsWith("!");
parseRanges(in.ranges, scanner);
} else {
throw new RuntimeException(
"Unexpected token '" + token + "'");
}
relation.expr = new Expression(operand, modValue);
condition.relations.add(relation);
if (!scanner.hasNext("and|or")) {
break;
}
token = scanner.next();
if (token.equals("and")) {
condition = new Condition();
andSequence.conditions.add(condition);
} else {
andSequence = new AndConditionSequence();
this.conditions.add(andSequence);
}
}
}
static void parseRanges(Collection<Range> ranges,
Scanner scanner) {
boolean first = true;
while (true) {
if (!first) {
if (!scanner.hasNext(",.*")) {
break;
}
scanner.findWithinHorizon(",", 0);
}
String token =
scanner.findWithinHorizon("\\d+(?:\\.\\.\\d+)?", 0);
int period = token.indexOf('.');
if (period > 0) {
long start = Long.parseLong(token.substring(0, period));
long end = Long.parseLong(token.substring(period + 2));
ranges.add(new Range(start, end));
} else {
long value = Long.parseLong(token);
ranges.add(new Range(value));
}
first = false;
}
}
boolean matches(BigDecimal value) {
return conditions.stream().anyMatch(c -> c.matches(value));
}
}
private static final Map<Locale, List<PluralRule>> pluralRules =
new HashMap<>();
private static final Map<Locale, Path> dataFiles = new HashMap<>();
private static final Path cldrDir;
static {
String dir = System.getProperty("cldr");
if (dir == null) {
throw new RuntimeException(
"\"cldr\" system property must be set to root directory"
+ " of CLDR data. That data can be downloaded from"
+ "https://cldr.unicode.org/index/downloads or"
+ "https://github.com/unicode-org/cldr .");
}
cldrDir = Paths.get(dir);
}
private final XPath xpath;
public PeriodFormatter() {
this.xpath = XPathFactory.newInstance().newXPath();
}
private static InputSource createSource(Path path) {
return new InputSource(path.toUri().toASCIIString());
}
private Count countFor(BigDecimal amount,
Locale locale) {
synchronized (pluralRules) {
if (pluralRules.isEmpty()) {
Path pluralsFile = cldrDir.resolve(
Paths.get("common", "supplemental", "plurals.xml"));
NodeList rulesElements;
try {
rulesElements = (NodeList) xpath.evaluate(
"//plurals[#type='cardinal']/pluralRules",
createSource(pluralsFile),
XPathConstants.NODESET);
} catch (XPathException e) {
throw new RuntimeException(e);
}
int count = rulesElements.getLength();
for (int i = 0; i < count; i++) {
Element rulesElement = (Element) rulesElements.item(i);
String[] localeNames =
rulesElement.getAttribute("locales").split("\\s+");
NodeList ruleElements =
rulesElement.getElementsByTagName("pluralRule");
int ruleCount = ruleElements.getLength();
List<PluralRule> ruleList = new ArrayList<>(ruleCount);
for (int j = 0; j < ruleCount; j++) {
Element ruleElement = (Element) ruleElements.item(j);
ruleList.add(new PluralRule(
ruleElement.getAttribute("count"),
ruleElement.getTextContent()));
}
for (String localeName : localeNames) {
localeName = localeName.replace('_', '-');
pluralRules.put(
Locale.forLanguageTag(localeName),
ruleList);
}
}
}
}
Locale availableLocale = Locale.lookup(
Locale.LanguageRange.parse(locale.toLanguageTag()),
pluralRules.keySet());
if (availableLocale == null) {
availableLocale = Locale.ROOT;
}
List<PluralRule> rulesOfLocale = pluralRules.get(availableLocale);
for (PluralRule rule : rulesOfLocale) {
if (rule.matches(amount)) {
return rule.count;
}
}
throw new IllegalArgumentException("No plural rule matches " + amount);
}
private static List<Locale> listWithFallbacks(Locale locale) {
Collection<Locale> locales = new LinkedHashSet<>();
locales.add(locale);
Locale.Builder builder = new Locale.Builder();
builder.setLanguageTag(locale.toLanguageTag());
locales.add(builder.setVariant(null).build());
locales.add(builder.setRegion(null).build());
locales.add(builder.setScript(null).build());
locales.add(Locale.ROOT);
return new ArrayList<>(locales);
}
private Iterable<Path> dataFilesFor(Locale locale) {
synchronized (dataFiles) {
if (dataFiles.isEmpty()) {
Path dataFileDir = cldrDir.resolve(Paths.get("common", "main"));
try (DirectoryStream<Path> dir =
Files.newDirectoryStream(dataFileDir, "*.xml")) {
for (Path dataFile : dir) {
InputSource source = createSource(dataFile);
NodeList identityElements = (NodeList)
xpath.evaluate("/ldml/identity", source,
XPathConstants.NODESET);
Element identity = (Element) identityElements.item(0);
String lang =
xpath.evaluate("language/#type", identity);
String script =
xpath.evaluate("script/#type", identity);
String region =
xpath.evaluate("territory/#type", identity);
String variant =
xpath.evaluate("variant/#type", identity);
Locale dataFileLocale;
if (lang.equals("root")) {
dataFileLocale = Locale.ROOT;
} else {
Locale.Builder builder = new Locale.Builder();
builder.setLanguage(lang);
builder.setScript(script);
builder.setRegion(region);
builder.setVariant(variant);
dataFileLocale = builder.build();
}
dataFiles.put(dataFileLocale, dataFile);
}
} catch (IOException | XPathException e) {
throw new RuntimeException(e);
}
}
}
Collection<Locale> locales = listWithFallbacks(locale);
Collection<Path> dataFilesForLocale = new ArrayList<>();
for (Locale localeToCheck : locales) {
Path dataFile = dataFiles.get(localeToCheck);
if (dataFile != null) {
dataFilesForLocale.add(dataFile);
}
}
return dataFilesForLocale;
}
private Optional<Element> locateElement(Object source,
String path) {
try {
Element element = null;
while (true) {
NodeList elements;
if (source instanceof InputSource) {
elements = (NodeList) xpath.evaluate(path,
(InputSource) source, XPathConstants.NODESET);
} else {
elements = (NodeList) xpath.evaluate(path,
source, XPathConstants.NODESET);
}
if (elements.getLength() < 1) {
break;
}
element = (Element) elements.item(0);
NodeList list = (NodeList) xpath.evaluate("alias", element,
XPathConstants.NODESET);
if (list.getLength() == 0) {
// No more aliases to follow, so we've found our target.
break;
}
Element alias = (Element) list.item(0);
path = alias.getAttribute("path");
source = element;
}
return Optional.ofNullable(element);
} catch (XPathException e) {
throw new RuntimeException(e);
}
}
private Optional<String> readElement(Iterable<? extends Path> dataFiles,
String... paths)
throws XPathException {
Optional<Element> element = Optional.empty();
for (Path dataFile : dataFiles) {
Object source = createSource(dataFile);
element = Optional.empty();
for (String path : paths) {
element = locateElement(source, path);
if (!element.isPresent()) {
break;
}
source = element.get();
}
if (element.isPresent()) {
break;
}
}
return element.map(Element::getTextContent);
}
private String format(ChronoUnit units,
BigDecimal amount,
Locale locale) {
String type = null;
switch (units) {
case YEARS:
type = "duration-year";
break;
case MONTHS:
type = "duration-month";
break;
case WEEKS:
type = "duration-week";
break;
case DAYS:
type = "duration-day";
break;
default:
throw new IllegalArgumentException(
"Valid units are YEARS, MONTHS, WEEKS, and DAYS.");
}
Count count = countFor(amount, locale);
try {
Optional<String> formatPattern = readElement(dataFilesFor(locale),
"/ldml/units" +
"/unitLength[#type='long']" +
"/unit[#type='" + type + "']",
"unitPattern[#count='" + count.attributeValue() + "']");
if (formatPattern.isPresent()) {
String patternStr = formatPattern.get();
if (!patternStr.isEmpty()) {
MessageFormat format =
new MessageFormat(patternStr, locale);
return format.format(new Object[] { amount });
}
}
} catch (XPathException e) {
throw new RuntimeException(e);
}
throw new IllegalArgumentException(
"Could not find pattern for units " + units +
", amount " + amount + ", locale " + locale.toLanguageTag());
}
public String format(Period period,
Locale locale) {
return format(period, false, locale);
}
public String format(Period period,
boolean includeWeeks,
Locale locale) {
int years = period.getYears();
int months = period.getMonths();
int days = period.getDays();
List<String> parts = new ArrayList<>(3);
if (years != 0) {
BigDecimal value = BigDecimal.valueOf(years);
String yearStr = format(ChronoUnit.YEARS, value, locale);
parts.add(yearStr);
}
if (months != 0) {
BigDecimal value = BigDecimal.valueOf(months);
String monthStr = format(ChronoUnit.MONTHS, value, locale);
parts.add(monthStr);
}
if (includeWeeks) {
int weeks = days / 7;
if (weeks != 0) {
days %= 7;
BigDecimal value = BigDecimal.valueOf(weeks);
String weekStr = format(ChronoUnit.WEEKS, value, locale);
parts.add(weekStr);
}
}
if (days != 0) {
BigDecimal value = BigDecimal.valueOf(days);
String dayStr = format(ChronoUnit.DAYS, value, locale);
parts.add(dayStr);
}
return formatList(parts, locale);
}
private String formatList(List<?> parts,
Locale locale) {
if (parts.isEmpty()) {
return "";
}
int size = parts.size();
if (size == 1) {
return String.valueOf(parts.get(0));
}
Map<String, String> patternsByType = new HashMap<>();
for (Path dataFile : dataFilesFor(locale)) {
Object source = createSource(dataFile);
Optional<Element> listPatterns =
locateElement(source, "/ldml/listPatterns");
if (!listPatterns.isPresent()) {
continue;
}
Optional<Element> listPattern =
locateElement(listPatterns.get(), "listPattern[#type='unit']");
if (!listPattern.isPresent()) {
continue;
}
NodeList partList =
listPattern.get().getElementsByTagName("listPatternPart");
int count = partList.getLength();
for (int i = 0; i < count; i++) {
Element part = (Element) partList.item(i);
String type = part.getAttribute("type");
String pattern = part.getTextContent();
patternsByType.putIfAbsent(type, pattern);
}
}
if (size == 2 || size == 3) {
String pattern = patternsByType.get(String.valueOf(size));
if (pattern != null) {
MessageFormat format =new MessageFormat(pattern, locale);
return format.format(parts.toArray());
}
}
MessageFormat startFormat = new MessageFormat(
patternsByType.get("start"), locale);
MessageFormat middleFormat = new MessageFormat(
patternsByType.get("middle"), locale);
MessageFormat endFormat = new MessageFormat(
patternsByType.get("end"), locale);
String text = endFormat.format(
new Object[] { parts.get(size - 2), parts.get(size - 1) });
int index = size - 2;
while (--index > 0) {
text = middleFormat.format(new Object[] { parts.get(index), text });
}
text = startFormat.format(new Object[] { parts.get(index), text });
return text;
}
public static void main(String[] args) {
String localeStr = System.getProperty("locale");
Locale locale = localeStr != null ?
Locale.forLanguageTag(localeStr) : Locale.getDefault();
boolean showWeeks = Boolean.getBoolean("weeks");
PeriodFormatter formatter = new PeriodFormatter();
for (String arg : args) {
Period period = Period.parse(arg);
String s = formatter.format(period, showWeeks, locale);
System.out.println(arg + " => " + s);
}
}
}
Sorry twice it seems, but i'd leave this post here, that problem is a terrible fright about calender display name fields, back in 2010 i spent a couple of days looking for the hr,min,sec displayneames until i got info from Sun Microsystems forum there were no names for some. The API doc info almost says that, just not clearly. Niether does it say anything clearly for java.time.temporal.TemporalField and java.time.temporal.ChronoField
and its method getDisplayName(Locale locale)
Sorry(deleted other answer), I thought you meant "time" itself, you meant "time UNITS" that are part of java TemporalUnit of the Duration is a Temporal with TemporalUnit's AND do have "name retrieval methods" but translated, no.
However, java.util.Calendar also has "time units" AND can be instantiated with Locale and should have class fields and methods that will return "time unit" foreign language names
This is a test printer class for a field that does have a display name. The commented out code prints "null" returned by getDisplayName()
// CalendarTest.java
public class CalendarTest{
int[] ipt = {java.util.Calendar.DAY_OF_WEEK,java.util.Calendar.MONTH,java.util.Calendar.ERA,java.util.Calendar.AM_PM};
public CalendarTest(){
//java.time.LocalDateTime tm = java.time.LocalDateTime.now(); +java.util.Calendar.HOUR DAY_OF_WEEK
for(int sd=0;sd<ipt.length;sd++){
pr(ipt[sd],"fr");
}
for(int sd1=0;sd1<ipt.length;sd1++){
pr(ipt[sd1],"zh");
}
for(int sd2=0;sd2<ipt.length;sd2++){
pr(ipt[sd2],"ar");
}
/*
//java.util.Date dt = cal.getTime();
System.out.println(""+cal.get(java.util.Calendar.HOUR));
String fieldname = cal.getDisplayName(java.util.Calendar.HOUR_OF_DAY, java.util.Calendar.SHORT, new java.util.Locale("en"));
System.out.println(fieldname);
fieldname = cal.getDisplayName(java.util.Calendar.HOUR_OF_DAY, java.util.Calendar.LONG, new java.util.Locale("en"));
System.out.println(fieldname);
*/
}//enconstr
public void pr(int fieldint,String loc){
java.util.Calendar cal = java.util.Calendar.getInstance(new java.util.Locale(loc));
java.util.Map<String,Integer> mp = cal.getDisplayNames(fieldint , java.util.Calendar.ALL_STYLES, new java.util.Locale(loc));
if(mp.isEmpty()==true){
System.out.println("Is Empty");
}else{
System.out.println("Contains");
}
java.util.Set<String> st = mp.keySet();
Object[] sta = st.toArray();
for(int fs=0;fs<sta.length;fs++){
System.out.println("A VALUE : "+sta[fs]);
}
}//enmeth
public static void main(String[] args){
new CalendarTest();
}//enmain
}//enclss
/* prints Special field values and in Locale but not field names
bash-5.1$ java CalendarTest
Contains
A VALUE : dimanche
A VALUE : mercredi
A VALUE : vendredi
A VALUE : samedi
A VALUE : jeudi
A VALUE : lundi
A VALUE : mardi
A VALUE : dim.
A VALUE : jeu.
A VALUE : lun.
A VALUE : mar.
A VALUE : mer.
A VALUE : sam.
A VALUE : ven.
Contains
A VALUE : septembre
A VALUE : décembre
A VALUE : novembre
A VALUE : février
A VALUE : janvier
A VALUE : juillet
A VALUE : octobre
A VALUE : avril
A VALUE : févr.
A VALUE : janv.
A VALUE : juil.
A VALUE : sept.
A VALUE : août
A VALUE : avr.
A VALUE : déc.
A VALUE : juin
A VALUE : mars
A VALUE : nov.
A VALUE : oct.
A VALUE : mai
Contains
A VALUE : ap. J.-C.
A VALUE : av. J.-C.
A VALUE : BC
A VALUE : A
A VALUE : B
Contains
A VALUE : AM
A VALUE : PM
A VALUE : a
A VALUE : p
Contains
A VALUE : 星期一
A VALUE : 星期三
A VALUE : 星期二
A VALUE : 星期五
A VALUE : 星期六
A VALUE : 星期四
A VALUE : 星期日
A VALUE : 一
A VALUE : 三
A VALUE : 二
A VALUE : 五
A VALUE : 六
A VALUE : 四
A VALUE : 日
Contains
A VALUE : 10月
A VALUE : 11月
A VALUE : 12月
A VALUE : 十一月
A VALUE : 十二月
A VALUE : 10
A VALUE : 11
A VALUE : 12
A VALUE : 1月
A VALUE : 2月
A VALUE : 3月
A VALUE : 4月
A VALUE : 5月
A VALUE : 6月
A VALUE : 7月
A VALUE : 8月
A VALUE : 9月
A VALUE : 一月
A VALUE : 七月
A VALUE : 三月
A VALUE : 九月
A VALUE : 二月
A VALUE : 五月
A VALUE : 八月
A VALUE : 六月
A VALUE : 十月
A VALUE : 四月
A VALUE : 1
A VALUE : 2
A VALUE : 3
A VALUE : 4
A VALUE : 5
A VALUE : 6
A VALUE : 7
A VALUE : 8
A VALUE : 9
Contains
A VALUE : 公元前
A VALUE : AD
A VALUE : BC
A VALUE : 公元
A VALUE : A
A VALUE : B
Contains
A VALUE : 上午
A VALUE : 下午
A VALUE : a
A VALUE : p
Contains
A VALUE : الأربعاء
A VALUE : الثلاثاء
A VALUE : الاثنين
A VALUE : الجمعة
A VALUE : الخميس
A VALUE : الأحد
A VALUE : السبت
A VALUE : ث
A VALUE : ج
A VALUE : ح
A VALUE : خ
A VALUE : ر
A VALUE : س
A VALUE : ن
Contains
A VALUE : أكتوبر
A VALUE : ديسمبر
A VALUE : سبتمبر
A VALUE : فبراير
A VALUE : نوفمبر
A VALUE : أبريل
A VALUE : أغسطس
A VALUE : يناير
A VALUE : يوليو
A VALUE : يونيو
A VALUE : مارس
A VALUE : مايو
A VALUE : أبر
A VALUE : أغس
A VALUE : أكت
A VALUE : ديس
A VALUE : سبت
A VALUE : فبر
A VALUE : مار
A VALUE : ماي
A VALUE : نوف
A VALUE : ينا
A VALUE : يول
A VALUE : يون
A VALUE : أ
A VALUE : ب
A VALUE : د
A VALUE : س
A VALUE : غ
A VALUE : ف
A VALUE : ك
A VALUE : ل
A VALUE : م
A VALUE : ن
A VALUE : و
A VALUE : ي
Contains
A VALUE : ق.م
A VALUE : A
A VALUE : B
A VALUE : م
Contains
A VALUE : a
A VALUE : p
A VALUE : ص
A VALUE : م
bash-5.1$
*/
The below code does not operate (as OleV.V. said) because some fields do not get a SHORT or LONG display name.
java.util.Calendar cal = java.util.Calendar.getInstance( java.util.TimeZone.getTimeZone("Asia/Shanghai") , java.util.Locale.CHINA);
String hourof = cal.getDisplayName(java.util.Calendar.HOUR , java.util.Calendar.LONG , java.util.Locale.CHINA);
Displaying it in html use attribute lang="zh" in the text rendering element tag e.g. div , input , td
UTF-8 is the normal charset to use bound to a locale although for Chinese it may be UTF-16
Take a look at the "field summary" in the API docs for java.util.Calendar.
Week, appears to be a problem but int SHORT could display simply "week" perhaps.
I'm developing an application that should verify signatures of pdf files. The application should detect full history of updates done on the file content before each signature is applied.
For example:
Signer 1 signed the plain pdf file
Signer 2 added comment to the signed file, then signed it
How can application detect that Signer 2 added a comment before his signature.
I have tried to use itext and pdfbox
As already explained in a comment, neither iText nor PDFBox bring along a high-level API telling you what changed in an incremental update in terms of UI objects (comments, text content, ...).
You can use them to render the different revisions of the PDF as bitmaps and compare those images.
Or you can use them to tell you the changes in terms of low level COS objects (dictionaries, arrays, numbers, strings, ...).
But analyzing the changes in those images or low level objects and determining their meaning in terms of UI objects, that e.g. a comment and only a comment has been added, is highly non-trivial.
In response you asked
Can you explain more, how can I detect changes in low level COS objects.
What to Compare And What Changes to Consider
First of all you have to be clear about what document states you can compare to detect changes.
The PDF format allows to append changes to a PDF in so called incremental updates. This allows changes to signed documents without cryptographically breaking those signatures as the original signed bytes are left as is:
There can be more incremental updates in-between, though, which are not signed; e.g. the "Changes for version 2" might include multiple incremental updates.
One might consider comparing the revisions created by arbitrary incremental updates. The problem here is, though, that you cannot identify the person who applied an incremental update without signature.
Thus, it usually makes more sense to compare the signed revisions only and to hold each signer responsible for all changes since the previous signed revision. The only exception here is the whole file which as the current version of the PDF is of special interest even if it there is no signature covering all of it.
Next you have to decide what you consider a change. In particular:
Is every object override in an incremental update a change? Even those that override the original object with an identical copy?
What about changes that make a direct object indirect (or vice versa) but keep all contents and references intact?
What about addition of new objects that are not referred to from anywhere in the standard structure?
What about addition of objects that are not referenced from the cross reference streams or tables?
What about addition of data that's not following PDF syntax at all?
If you are indeed interested in such changes, too, existing PDF libraries out-of-the-box usually don't provide you the means to determine them; you most likely will at least have to change their code for traversing the chain of cross reference tables/streams or even analyze the file bytes in the update directly.
If you are not interested in such changes, though, there usually is no need to change or replace library routines.
As the enumerated and similar changes make no difference when the PDF is processed by specification conform PDF processors, one can usually ignore such changes.
If this is your position, too, the following example tool might give you a starting point.
An Example Tool Based on iText 7
With the limitations explained above you can compare signed revisions of a PDF using iText 7 without changes to the library by loading the revisions to compare into separate PdfDocument instances and recursively comparing the PDF objects starting with the trailer.
I once implemented this as a small helper tool for personal use (so it is not completely finished yet, more work-in-progress). First there is the base class that allows comparing two arbitrary documents:
public class PdfCompare {
public static void main(String[] args) throws IOException {
System.out.printf("Comparing:\n* %s\n* %s\n", args[0], args[1]);
try ( PdfDocument pdfDocument1 = new PdfDocument(new PdfReader(args[0]));
PdfDocument pdfDocument2 = new PdfDocument(new PdfReader(args[1])) ) {
PdfCompare pdfCompare = new PdfCompare(pdfDocument1, pdfDocument2);
pdfCompare.compare();
List<Difference> differences = pdfCompare.getDifferences();
if (differences == null || differences.isEmpty()) {
System.out.println("No differences found.");
} else {
System.out.printf("%d differences found:\n", differences.size());
for (Difference difference : pdfCompare.getDifferences()) {
for (String element : difference.getPath()) {
System.out.print(element);
}
System.out.printf(" - %s\n", difference.getDescription());
}
}
}
}
public interface Difference {
List<String> getPath();
String getDescription();
}
public PdfCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
trailer1 = pdfDocument1.getTrailer();
trailer2 = pdfDocument2.getTrailer();
}
public void compare() {
LOGGER.info("Starting comparison");
try {
compared.clear();
differences.clear();
LOGGER.info("START COMPARE");
compare(trailer1, trailer2, Collections.singletonList("trailer"));
LOGGER.info("START SHORTEN PATHS");
shortenPaths();
} finally {
LOGGER.info("Finished comparison and shortening");
}
}
public List<Difference> getDifferences() {
return differences;
}
class DifferenceImplSimple implements Difference {
DifferenceImplSimple(PdfObject object1, PdfObject object2, List<String> path, String description) {
this.pair = Pair.of(object1, object2);
this.path = path;
this.description = description;
}
#Override
public List<String> getPath() {
List<String> byPair = getShortestPath(pair);
return byPair != null ? byPair : shorten(path);
}
#Override public String getDescription() { return description; }
final Pair<PdfObject, PdfObject> pair;
final List<String> path;
final String description;
}
void compare(PdfObject object1, PdfObject object2, List<String> path) {
LOGGER.debug("Comparing objects at {}.", path);
if (object1 == null && object2 == null)
{
LOGGER.debug("Both objects are null at {}.", path);
return;
}
if (object1 == null) {
differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 1"));
LOGGER.info("Object in document 1 is missing at {}.", path);
return;
}
if (object2 == null) {
differences.add(new DifferenceImplSimple(object1, object2, path, "Missing in document 2"));
LOGGER.info("Object in document 2 is missing at {}.", path);
return;
}
if (object1.getType() != object2.getType()) {
differences.add(new DifferenceImplSimple(object1, object2, path,
String.format("Type difference, %s in document 1 and %s in document 2",
getTypeName(object1.getType()), getTypeName(object2.getType()))));
LOGGER.info("Objects have different types at {}, {} and {}.", path, getTypeName(object1.getType()), getTypeName(object2.getType()));
return;
}
switch (object1.getType()) {
case PdfObject.ARRAY:
compareContents((PdfArray) object1, (PdfArray) object2, path);
break;
case PdfObject.DICTIONARY:
compareContents((PdfDictionary) object1, (PdfDictionary) object2, path);
break;
case PdfObject.STREAM:
compareContents((PdfStream)object1, (PdfStream)object2, path);
break;
case PdfObject.BOOLEAN:
case PdfObject.INDIRECT_REFERENCE:
case PdfObject.LITERAL:
case PdfObject.NAME:
case PdfObject.NULL:
case PdfObject.NUMBER:
case PdfObject.STRING:
compareContentsSimple(object1, object2, path);
break;
default:
differences.add(new DifferenceImplSimple(object1, object2, path, "Unknown object type " + object1.getType() + "; cannot compare"));
LOGGER.warn("Unknown object type at {}, {}.", path, object1.getType());
break;
}
}
void compareContents(PdfArray array1, PdfArray array2, List<String> path) {
int count1 = array1.size();
int count2 = array2.size();
if (count1 < count2) {
differences.add(new DifferenceImplSimple(array1, array2, path, "Document 1 misses " + (count2-count1) + " array entries"));
LOGGER.info("Array in document 1 is missing {} entries at {} for {}.", (count2-count1), path);
}
if (count1 > count2) {
differences.add(new DifferenceImplSimple(array1, array2, path, "Document 2 misses " + (count1-count2) + " array entries"));
LOGGER.info("Array in document 2 is missing {} entries at {} for {}.", (count1-count2), path);
}
if (alreadyCompared(array1, array2, path)) {
return;
}
int count = Math.min(count1, count2);
for (int i = 0; i < count; i++) {
compare(array1.get(i), array2.get(i), join(path, String.format("[%d]", i)));
}
}
void compareContents(PdfDictionary dictionary1, PdfDictionary dictionary2, List<String> path) {
List<PdfName> missing1 = new ArrayList<PdfName>(dictionary2.keySet());
missing1.removeAll(dictionary1.keySet());
if (!missing1.isEmpty()) {
differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 1 misses dictionary entries for " + missing1));
LOGGER.info("Dictionary in document 1 is missing entries at {} for {}.", path, missing1);
}
List<PdfName> missing2 = new ArrayList<PdfName>(dictionary1.keySet());
missing2.removeAll(dictionary2.keySet());
if (!missing2.isEmpty()) {
differences.add(new DifferenceImplSimple(dictionary1, dictionary2, path, "Document 2 misses dictionary entries for " + missing2));
LOGGER.info("Dictionary in document 2 is missing entries at {} for {}.", path, missing2);
}
if (alreadyCompared(dictionary1, dictionary2, path)) {
return;
}
List<PdfName> common = new ArrayList<PdfName>(dictionary1.keySet());
common.retainAll(dictionary2.keySet());
for (PdfName name : common) {
compare(dictionary1.get(name), dictionary2.get(name), join(path, name.toString()));
}
}
void compareContents(PdfStream stream1, PdfStream stream2, List<String> path) {
compareContents((PdfDictionary)stream1, (PdfDictionary)stream2, path);
byte[] bytes1 = stream1.getBytes();
byte[] bytes2 = stream2.getBytes();
if (!Arrays.equals(bytes1, bytes2)) {
differences.add(new DifferenceImplSimple(stream1, stream2, path, "Stream contents differ"));
LOGGER.info("Stream contents differ at {}.", path);
}
}
void compareContentsSimple(PdfObject object1, PdfObject object2, List<String> path) {
// vvv--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
if (object1 instanceof PdfNumber)
((PdfNumber)object1).getValue();
if (object2 instanceof PdfNumber)
((PdfNumber)object2).getValue();
// ^^^--- work-around for DEVSIX-4931, likely to be fixed in 7.1.15
if (!object1.equals(object2)) {
if (object1 instanceof PdfString) {
String string1 = object1.toString();
if (string1.length() > 40)
string1 = string1.substring(0, 40) + '\u22EF';
string1 = sanitize(string1);
String string2 = object2.toString();
if (string2.length() > 40)
string2 = string2.substring(0, 40) + '\u22EF';
string2 = sanitize(string2);
differences.add(new DifferenceImplSimple(object1, object2, path, String.format("String values differ, '%s' and '%s'", string1, string2)));
LOGGER.info("String values differ at {}, '{}' and '{}'.", path, string1, string2);
} else {
differences.add(new DifferenceImplSimple(object1, object2, path, String.format("Object values differ, '%s' and '%s'", object1, object2)));
LOGGER.info("Object values differ at {}, '{}' and '{}'.", path, object1, object2);
}
}
}
String sanitize(CharSequence string) {
char[] sanitized = new char[string.length()];
for (int i = 0; i < sanitized.length; i++) {
char c = string.charAt(i);
if (c >= 0 && c < ' ')
c = '\uFFFD';
sanitized[i] = c;
}
return new String(sanitized);
}
String getTypeName(byte type) {
switch (type) {
case PdfObject.ARRAY: return "ARRAY";
case PdfObject.BOOLEAN: return "BOOLEAN";
case PdfObject.DICTIONARY: return "DICTIONARY";
case PdfObject.LITERAL: return "LITERAL";
case PdfObject.INDIRECT_REFERENCE: return "REFERENCE";
case PdfObject.NAME: return "NAME";
case PdfObject.NULL: return "NULL";
case PdfObject.NUMBER: return "NUMBER";
case PdfObject.STREAM: return "STREAM";
case PdfObject.STRING: return "STRING";
default:
return "UNKNOWN";
}
}
List<String> join(List<String> path, String element) {
String[] array = path.toArray(new String[path.size() + 1]);
array[array.length-1] = element;
return Arrays.asList(array);
}
boolean alreadyCompared(PdfObject object1, PdfObject object2, List<String> path) {
Pair<PdfObject, PdfObject> pair = Pair.of(object1, object2);
if (compared.containsKey(pair)) {
//LOGGER.debug("Objects already compared at {}, previously at {}.", path, compared.get(pair));
Set<List<String>> paths = compared.get(pair);
boolean alreadyPresent = false;
// List<List<String>> toRemove = new ArrayList<>();
// for (List<String> formerPath : paths) {
// for (int i = 0; ; i++) {
// if (i == path.size()) {
// toRemove.add(formerPath);
// System.out.print('.');
// break;
// }
// if (i == formerPath.size()) {
// alreadyPresent = true;
// System.out.print(':');
// break;
// }
// if (!path.get(i).equals(formerPath.get(i)))
// break;
// }
// }
// paths.removeAll(toRemove);
if (!alreadyPresent)
paths.add(path);
return true;
}
compared.put(pair, new HashSet<>(Collections.singleton(path)));
return false;
}
List<String> getShortestPath(Pair<PdfObject, PdfObject> pair) {
Set<List<String>> paths = compared.get(pair);
//return (paths == null) ? null : Collections.min(paths, pathComparator);
return (paths == null || paths.isEmpty()) ? null : shortened.get(paths.stream().findFirst().get());
}
void shortenPaths() {
List<Map<List<String>, SortedSet<List<String>>>> data = new ArrayList<>();
for (Set<List<String>> set : compared.values()) {
SortedSet<List<String>> sortedSet = new TreeSet<List<String>>(pathComparator);
sortedSet.addAll(set);
for (List<String> path : sortedSet) {
while (path.size() >= data.size()) {
data.add(new HashMap<>());
}
SortedSet<List<String>> former = data.get(path.size()).put(path, sortedSet);
if (former != null) {
LOGGER.error("Path not well-defined for {}", path);
}
}
}
for (int pathSize = 3; pathSize < data.size(); pathSize++) {
for (Map.Entry<List<String>, SortedSet<List<String>>> pathEntry : data.get(pathSize).entrySet()) {
List<String> path = pathEntry.getKey();
SortedSet<List<String>> equivalents = pathEntry.getValue();
for (int subpathSize = 2; subpathSize < pathSize; subpathSize++) {
List<String> subpath = path.subList(0, subpathSize);
List<String> remainder = path.subList(subpathSize, pathSize);
SortedSet<List<String>> subequivalents = data.get(subpathSize).get(subpath);
if (subequivalents != null && subequivalents.size() > 1) {
List<String> subequivalent = subequivalents.first();
if (subequivalent.size() < subpathSize) {
List<String> replacement = join(subequivalent, remainder);
if (equivalents.add(replacement)) {
data.get(replacement.size()).put(replacement, equivalents);
}
}
}
}
}
}
shortened.clear();
for (Map<List<String>, SortedSet<List<String>>> singleLengthData : data) {
for (Map.Entry<List<String>, SortedSet<List<String>>> entry : singleLengthData.entrySet()) {
List<String> path = entry.getKey();
List<String> shortenedPath = entry.getValue().first();
shortened.put(path, shortenedPath);
}
}
}
List<String> join(List<String> path, List<String> elements) {
String[] array = path.toArray(new String[path.size() + elements.size()]);
for (int i = 0; i < elements.size(); i++) {
array[path.size() + i] = elements.get(i);
}
return Arrays.asList(array);
}
List<String> shorten(List<String> path) {
List<String> shortPath = path;
for (int subpathSize = path.size(); subpathSize > 2; subpathSize--) {
List<String> subpath = path.subList(0, subpathSize);
List<String> shortSubpath = shortened.get(subpath);
if (shortSubpath != null && shortSubpath.size() < subpathSize) {
List<String> remainder = path.subList(subpathSize, path.size());
List<String> replacement = join(shortSubpath, remainder);
if (replacement.size() < shortPath.size())
shortPath = replacement;
}
}
return shortPath;
}
final static Logger LOGGER = LoggerFactory.getLogger(PdfCompare.class);
final PdfDictionary trailer1;
final PdfDictionary trailer2;
final Map<Pair<PdfObject, PdfObject>, Set<List<String>>> compared = new HashMap<>();
final List<Difference> differences = new ArrayList<>();
final Map<List<String>, List<String>> shortened = new HashMap<>();
final static Comparator<List<String>> pathComparator = new Comparator<List<String>>() {
#Override
public int compare(List<String> o1, List<String> o2) {
int compare = Integer.compare(o1.size(), o2.size());
if (compare != 0)
return compare;
for (int i = 0; i < o1.size(); i++) {
compare = o1.get(i).compareTo(o2.get(i));
if (compare != 0)
return compare;
}
return 0;
}
};
}
(PdfCompare.java)
The tool to use this code for revision comparison is a subclass thereof:
public class PdfRevisionCompare extends PdfCompare {
public static void main(String[] args) throws IOException {
for (String arg : args) {
System.out.printf("\nComparing revisions of: %s\n***********************\n", args[0]);
try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(arg))) {
SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
List<String> signatureNames = signatureUtil.getSignatureNames();
if (signatureNames.isEmpty()) {
System.out.println("No signed revisions detected. (no AcroForm)");
continue;
}
String previousRevision = signatureNames.get(0);
PdfDocument previousDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(previousRevision)));
System.out.printf("* Initial signed revision: %s\n", previousRevision);
for (int i = 1; i < signatureNames.size(); i++) {
String currentRevision = signatureNames.get(i);
PdfDocument currentDocument = new PdfDocument(new PdfReader(signatureUtil.extractRevision(currentRevision)));
showDifferences(previousDocument, currentDocument);
System.out.printf("* Next signed revision (%d): %s\n", i+1, currentRevision);
previousDocument.close();
previousDocument = currentDocument;
previousRevision = currentRevision;
}
if (signatureUtil.signatureCoversWholeDocument(previousRevision)) {
System.out.println("No unsigned updates.");
} else {
showDifferences(previousDocument, pdfDocument);
System.out.println("* Final unsigned revision");
}
previousDocument.close();
}
}
}
static void showDifferences(PdfDocument previousDocument, PdfDocument currentDocument) {
PdfRevisionCompare pdfRevisionCompare = new PdfRevisionCompare(previousDocument, currentDocument);
pdfRevisionCompare.compare();
List<Difference> differences = pdfRevisionCompare.getDifferences();
if (differences == null || differences.isEmpty()) {
System.out.println("No differences found.");
} else {
System.out.printf("%d differences found:\n", differences.size());
for (Difference difference : differences) {
for (String element : difference.getPath()) {
System.out.print(element);
}
System.out.printf(" - %s\n", difference.getDescription());
}
}
}
public PdfRevisionCompare(PdfDocument pdfDocument1, PdfDocument pdfDocument2) {
super(pdfDocument1, pdfDocument2);
}
}
(PdfRevisionCompare.java)
I have a JSON response. I need to count values that are null and then values that are 0. The JSON looks something like:
{
“stuffA“:{“a”: {“b”: 1.12343, “c”: “2019-01-29”, “d”: null } },
”stuffB”:{ "e":{}, "f":{}, "g":{}},
”stuffC”:[ {"h":{}},{"i":{}} ],
”stuffD”:[ {“j”:{}},{“k”:{}},{“l”:{}},{“m”:{}}],
}
I tried convert JSONObject to HashMap And look for frequency of null then 0 but doesn’t seem to work.
map = (Map<String,Object>) gson.fromJson(json, map.getClass());
int count1 = Collections.frequency(map.values(), null);
int count2 = Collections.frequency(map.values(), 0);
Am I going about this correctly or is there a more effective way?
boolean objectIsNull = yourJsonObject.isNull("stuffA");
you can try this inside a for
first add following dependency to your pom.xml
<dependency>
<groupId>com.github.wnameless.json</groupId>
<artifactId>json-flattener</artifactId>
<version>0.8.1</version>
</dependency>
first i transform json to a flat map object then count the count null and empty objects:
public class Solution {
public static void main(String[] args) {
String json = "{\n" +
" \"stuffA\":{\"a\": {\"b\": 1.12343, \"c\": \"2019-01-29\", \"d\": null } },\n" +
" \"stuffB\":{ \"e\":{}, \"f\":{}, \"g\":{}},\n" +
" \"stuffC\":[ {\"h\":{}},{\"i\":{}} ],\n" +
" \"stuffD\":[ {\"j\":{}},{\"k\":{}},{\"l\":{}},{\"m\":{}}]\n" +
"}";
System.out.println(countZeroAndNull(json));
}
public static long countZeroAndNull(String json) {
Map<String, Object> flattenJson = JsonFlattener.flattenAsMap(json);
int count = 0;
for (String s : flattenJson.keySet()) {
if (flattenJson.get(s) == null) {
count++;
} else if (flattenJson.get(s) instanceof LinkedHashMap) {
if (((LinkedHashMap) flattenJson.get(s)).size() < 1) {
count++;
}
}
}
return count;
}
}
it your example it prints 10 as output
I have a little helper util that reads a csv into a pojo. For the most part it works really fine. Now I have an issue with enums.
I am able to fill:
an enum
a list of enums
an array
But I have a problem with an array of enums. Here is a code snippet of some of these special cases:
public void fillPojo(Object pojo) {
// use setter/getter as well - using beanutils
for(PropertyDescriptor pd : PropertyUtils.getPropertyDescriptors(pojo.getClass())) {
if(pd.getName().equals("class")|| pd.getReadMethod() == null)
continue;
// get the value (based on the property name)
String value = this.get(pd.getName());
if(value == null || value.equals("null"))
continue;
try {
// this works for normal lists and list of any enum
if(pd.getPropertyType().isAssignableFrom(List.class)) {
List<String> values = new ArrayList<>();
for(String s : value.split(","))
values.add(s);
pd.getWriteMethod().invoke(pojo, ConvertUtils.convert(values, pd.getPropertyType()));
}
else if(pd.getPropertyType().isArray()) {
///////////////////////// this throws a conversionException
List<String> values = new ArrayList<>();
for(String s : value.split(","))
values.add(s);
Object[] objs = new Object[values.size()];
for(int i = 0; i < objs.length; i++) {
if(StringUtils.isBlank(values.get(i)))
objs[i] = null;
else {
objs[i] = ConvertUtils.convert(values.get(i), pd.getPropertyType().getComponentType());
}
}
pd.getWriteMethod().invoke(pojo, objs);
/////////////////////////
}
else
if(pd.getPropertyType().isEnum()) {
if(StringUtils.isEmpty(value) || "null".equalsIgnoreCase(value))
pd.getWriteMethod().invoke(pojo, (Object)null);
else
pd.getWriteMethod().invoke(pojo, Enum.valueOf(pd.getPropertyType().asSubclass(Enum.class), value));
}
else
pd.getWriteMethod().invoke(pojo, ConvertUtils.convert(value, pd.getPropertyType()));
} catch (NullPointerException | IllegalAccessException | IllegalArgumentException
| ConversionException | InvocationTargetException e) {
System.err.println("'" + pojo.getClass().getSimpleName() + "' Problem while setting: " + pd.getName() + " with value " + value + " type: " + pd.getPropertyType().getSimpleName() + ":" + e.getMessage());
e.printStackTrace();
}
}
I tried different approaches to create the enum, but I cannot seem to be able to correctly create a list of enums and setting them without throwing an exception.
Seems you pass into the setter an array of invalid type, e.g. you have bean setter:
public void setMyEnumArray(MyEnum[] array) {...}
then your call of 'pd.getWriteMethod().invoke(pojo, objs)' would be similar to:
Object[] objs = new Object[values.size()];
...
pojo.setMyEnumArray(objs);
Thus, you should convert Object[] into MyEnum[] before calling this setter:
Object[] objs = new Object[values.size()];
...
objs = Arrays.copyOf(objs, objs.length, (Class) pd.getPropertyType());
pd.getWriteMethod().invoke(pojo, objs);
In java, I want to compare two maps, like below, do we have existing API to do this ?
Thanks
Map<String, String> beforeMap ;
beforeMap.put("a", "1");
beforeMap.put("b", "2");
beforeMap.put("c", "3");
Map<String, String> afterMap ;
afterMap.put("a", "1");
afterMap.put("c", "333");
//--- it should give me:
b is missing, c value changed from '3' to '333'
I'd use removeAll() functionality of Set to to do set differences of keys to find additions and deletions. Actual changes can be detected by doing a set difference using the entry set as HashMap.Entry implements equals() using both key and value.
Set<String> removedKeys = new HashSet<String>(beforeMap.keySet());
removedKeys.removeAll(afterMap.keySet());
Set<String> addedKeys = new HashSet<String>(afterMap.keySet());
addedKeys.removeAll(beforeMap.keySet());
Set<Entry<String, String>> changedEntries = new HashSet<Entry<String, String>>(
afterMap.entrySet());
changedEntries.removeAll(beforeMap.entrySet());
System.out.println("added " + addedKeys);
System.out.println("removed " + removedKeys);
System.out.println("changed " + changedEntries);
Output
added []
removed [b]
changed [c=333]
The Guava Maps class has some methods for calulating the differences between a pair of maps. However, these methods give you a data structure representing the differences not a pretty-printed string.
There isn't any out of the box component to help with that. You'll probably have to code it unfortunately. The good news is the logic is pretty easy.
Depending upon your particular needs, you might also consider using other applications designed to do this work, like diff. You could write the two maps to two different files, and diff the files.
String output = new String();
for (String key:beforeMap.getKeys()){
String beforeValue = beforeMap.getValue(key);
String afterValue = afterMap.getValue(key);
//nullsafe
if(beforeValue.equals(afterValue){}
else if (afterValue == null){
output = output + key + " is missing, ";
continue;
}else {
output = output + key + " has changed from " + beforeValue + " to " + afterValue + " , ";
}
afterMap.remove(key);
}
for (String key:afterMap.getKeys()){
output = output + key + " was added with value " + afterMap.getValue(key) + ", ";
}
if(output == null){
output = "Same map";
}
output = output.substring(0,output.length-2);
System.out.println(output);
You could use a custom object that contains the key and the value (actually Map does this internally, hidden from the user, so we can't use that)
Put these tuples into a Set
To compare two sets, convert them both to arrays, sort the arrays and walk both arrays from begin to end in parallel, stepping down the first array if it's key is smaller than the key in the second array, and vise versa.
class Tuple implements Comparable<Tuple>
{
public String key;
public String value;
public Tuple(String key, String value)
{
this.key = key;
this.value = value;
}
#Override
public int compareTo(Tuple o)
{
return key.compareTo(o.key);
}
}
public static void main(String[] args)
{
// TreeSet is already sorted. If you use HashSet, use Arrays.sort()
Set<Tuple> beforeSet = new TreeSet<>();
beforeSet.add(new Tuple("a", "1"));
beforeSet.add(new Tuple("b", "2"));
beforeSet.add(new Tuple("c", "4"));
Set<Tuple> afterSet = new TreeSet<>();
afterSet.add(new Tuple("a", "1"));
afterSet.add(new Tuple("c", "333"));
afterSet.add(new Tuple("aa", "4"));
Tuple[] beforeArray = beforeSet.toArray(new Tuple[beforeSet.size()]);
Tuple[] afterArray = afterSet.toArray(new Tuple[afterSet.size()]);
int beforePtr = 0;
int afterPtr = 0;
while (beforePtr < beforeArray.length || afterPtr < afterArray.length)
{
int difference = afterPtr >= afterArray.length? -1 : beforePtr >= beforeArray.length? 1 : beforeArray[beforePtr].compareTo(afterArray[afterPtr]);
if (difference == 0)
{
if (!beforeArray[beforePtr].value.equals(afterArray[afterPtr].value))
{
System.out.println(beforeArray[beforePtr].key + " value changed from '" + beforeArray[beforePtr].value + "' to '" + afterArray[afterPtr].value + "'");
}
beforePtr++;
afterPtr++;
}
else if (difference < 0)
{
System.out.println(beforeArray[beforePtr].key + " is missing");
beforePtr++;
}
else
{
System.out.println(afterArray[afterPtr].key + " is added");
afterPtr++;
}
}
}
#user595234 To Compare the two Maps you can add the keys of a map to list and with those 2 lists you can use the methods retainAll() and removeAll() and add them to another common keys list and different keys list. Using the keys of the common list and different list you can iterate through map, using equals you can compare the maps.
public class Demo
{
public static void main(String[] args)
{
Map<String, String> beforeMap = new HashMap<String, String>();
beforeMap.put("a", "1");
beforeMap.put("b", "2");
beforeMap.put("c", "3");
Map<String, String> afterMap = new HashMap<String, String>();
afterMap.put("a", "1");
afterMap.put("c", "333");
System.out.println("Before "+beforeMap);
System.out.println("After "+afterMap);
List<String> beforeList = getAllKeys(beforeMap);
List<String> afterList = getAllKeys(afterMap);
List<String> commonList1 = beforeList;
List<String> commonList2 = afterList;
List<String> diffList1 = getAllKeys(beforeMap);
List<String> diffList2 = getAllKeys(afterMap);
commonList1.retainAll(afterList);
commonList2.retainAll(beforeList);
diffList1.removeAll(commonList1);
diffList2.removeAll(commonList2);
System.out.println("Common List of before map "+commonList1);
System.out.println("Common List of after map "+commonList2);
System.out.println("Diff List of before map "+diffList1);
System.out.println("Diff List of after map "+diffList2);
if(commonList1!=null & commonList2!=null) // athough both the size are same
{
for (int i = 0; i < commonList1.size(); i++)
{
if ((beforeMap.get(commonList1.get(i))).equals(afterMap.get(commonList1.get(i))))
{
System.out.println("Equal: Before- "+ beforeMap.get(commonList1.get(i))+" After- "+afterMap.get(commonList1.get(i)));
}
else
{
System.out.println("Unequal: Before- "+ beforeMap.get(commonList1.get(i))+" After- "+afterMap.get(commonList1.get(i)));
}
}
}
if (CollectionUtils.isNotEmpty(diffList1))
{
for (int i = 0; i < diffList1.size(); i++)
{
System.out.println("Values present only in before map: "+beforeMap.get(diffList1.get(i)));
}
}
if (CollectionUtils.isNotEmpty(diffList2))
{
for (int i = 0; i < diffList2.size(); i++)
{
System.out.println("Values present only in after map: "+afterMap.get(diffList2.get(i)));
}
}
}
/** getAllKeys API adds the keys of the map to a list */
private static List<String> getAllKeys(Map<String, String> map1)
{
List<String> key = new ArrayList<String>();
if (map1 != null)
{
Iterator<String> mapIterator = map1.keySet().iterator();
while (mapIterator.hasNext())
{
key.add(mapIterator.next());
}
}
return key;
}
}
The below code will give you this output:
Before: {b=2, c=3, a=1}
After: {c=333, a=1}
Unequal: Before- 3 After- 333
Equal: Before- 1 After- 1
Values present only in before map: 2