From: Marco Zanon Date: Sat, 2 Mar 2024 17:22:29 +0000 (+0000) Subject: Renamed some methods and variables. X-Git-Tag: SVN-to-Git~9 X-Git-Url: https://gitweb.marcozanon.com/?a=commitdiff_plain;h=313250dfa87ff7d7eaa535e7d2591701376fa537;p=Macaco Renamed some methods and variables. --- diff --git a/10.x/CHANGELOG b/10.x/CHANGELOG new file mode 100644 index 0000000..cab32f4 --- /dev/null +++ b/10.x/CHANGELOG @@ -0,0 +1,38 @@ +Macaco +Copyright (c) 2009-2024 Marco Zanon . +See LICENSE for details. + +=================== +10.0.0 (2024-03-02) +=================== +* Renamed some methods and variables. + +------------------ +9.2.1 (2024-03-02) +------------------ +* Added missing checks. +* Fixed a bug which prevented full license files to be written properly. + +------------------ +9.2.0 (2024-02-29) +------------------ +* Added the licensing package and implemented the license manager. + +------------------ +9.1.0 (2024-02-29) +------------------ +* Implemented pretty print mode for MJsonArray and MJsonObject. +* Implemented automatic stripping of newline characters when loading JSON objects. +* Upgraded copyright notice to year 2024. + +================== +9.0.0 (2023-11-12) +================== +* Renamed some static methods. + +================== +8.0.0 (2023-08-09) +================== +* Switched from String to Path. +* Implemented log filter mark message appender. +* Replaced constant. diff --git a/10.x/LICENSE b/10.x/LICENSE new file mode 100644 index 0000000..1d99b31 --- /dev/null +++ b/10.x/LICENSE @@ -0,0 +1,6 @@ +Macaco +Copyright (c) 2009-2024 Marco Zanon . +Released under MIT license (https://opensource.org/licenses/MIT). + +Small portions inspired by other projects or web pages. +See source code for additional information. diff --git a/10.x/build.properties b/10.x/build.properties new file mode 100644 index 0000000..e69de29 diff --git a/10.x/build.xml b/10.x/build.xml new file mode 100644 index 0000000..8cec9f3 --- /dev/null +++ b/10.x/build.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/10.x/src/main/java/com/marcozanon/macaco/MConstants.java b/10.x/src/main/java/com/marcozanon/macaco/MConstants.java new file mode 100644 index 0000000..a64c57f --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/MConstants.java @@ -0,0 +1,33 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class MConstants extends MObject { + + /* Generic information. */ + + public static final String MACACO_VERSION = "10.x"; + + /* Environment configuration. */ + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + public static final String JSON_PRETTY_PRINT_MODE_EOL = "\r\n"; + public static final int JSON_PRETTY_PRINT_MODE_INDENTATION = 2; // characters + + /* Threads. */ + + public static final String LOG_FILTER_MARK_MESSAGE_APPENDER_THREAD_ID = "logFilterMarkMessageAppender"; + public static final long LOG_FILTER_MARK_MESSAGE_APPENDER_SLEEP_INTERVAL = 1000L; // milliseconds + + public static final long LOG_FILTER_MARK_INTERVAL = 60L; // seconds + public static final String LOG_FILTER_MARK_MESSAGE = "."; + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/MException.java b/10.x/src/main/java/com/marcozanon/macaco/MException.java new file mode 100644 index 0000000..92493b6 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/MException.java @@ -0,0 +1,29 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco; + +public abstract class MException extends Exception { + + /* */ + + public MException() { + super(); + } + + public MException(String message) { + super(message); + } + + public MException(Throwable error) { + super(error); + } + + public MException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/MInformation.java b/10.x/src/main/java/com/marcozanon/macaco/MInformation.java new file mode 100644 index 0000000..622f807 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/MInformation.java @@ -0,0 +1,52 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class MInformation extends MObject { + + /* Generic information. */ + + public static String getMacacoVersion() { + return MConstants.MACACO_VERSION; + } + + public static String getMacacoFullName() { + return "Macaco " + MInformation.getMacacoVersion(); + } + + public static String getMacacoCopyrightInformation() { + StringBuilder s = new StringBuilder(""); + // + s.append(MInformation.getMacacoFullName() + System.getProperty("line.separator")); + s.append("Copyright (c) 2009-2024 by Marco Zanon." + System.getProperty("line.separator")); + s.append("Released under MIT license (https://opensource.org/licenses/MIT)." + System.getProperty("line.separator")); + s.append("Small portions inspired by other projects or web pages. See source code for additional information."); + // + return s.toString(); + } + + /* Throwables. */ + + public static String getThrowableAsString(Throwable throwable) { + if (null == throwable) { + throw new IllegalArgumentException("Invalid 'throwable': null."); + } + // + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + // + throwable.printStackTrace(pw); + pw.flush(); + sw.flush(); + // + return sw.toString(); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/MObject.java b/10.x/src/main/java/com/marcozanon/macaco/MObject.java new file mode 100644 index 0000000..311751d --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/MObject.java @@ -0,0 +1,23 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco; + +public abstract class MObject { + + /* */ + + @Override + protected MObject clone() { + throw new UnsupportedOperationException("Please provide manually by yourself."); + } + + @Override + public String toString() { + throw new UnsupportedOperationException("Please use appropriate methods (if any)."); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MConversionException.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MConversionException.java new file mode 100644 index 0000000..f4c825f --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MConversionException.java @@ -0,0 +1,31 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +import com.marcozanon.macaco.MException; + +public abstract class MConversionException extends MException { + + /* */ + + public MConversionException() { + super(); + } + + public MConversionException(String message) { + super(message); + } + + public MConversionException(Throwable error) { + super(error); + } + + public MConversionException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java new file mode 100644 index 0000000..d0e6b05 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java @@ -0,0 +1,255 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.TimeZone; + +public class MDateConverter extends MObject { + + protected LinkedHashSet dateFormats = new LinkedHashSet(); + protected Locale locale = null; + protected TimeZone timeZone = null; + + /* */ + + public MDateConverter(String defaultDateFormat, Locale locale) { + this(defaultDateFormat, locale, TimeZone.getDefault()); + } + + public MDateConverter(String defaultDateFormat, Locale locale, TimeZone timeZone) { + super(); + // + this.addDateFormat(defaultDateFormat); + this.setLocale(locale); + this.setTimeZone(timeZone); + } + + public MDateConverter(LinkedHashSet dateFormats, Locale locale, TimeZone timeZone) { + super(); + // + this.setDateFormats(dateFormats); + this.setLocale(locale); + this.setTimeZone(timeZone); + } + + @Override + public MDateConverter clone() { + return new MDateConverter(this.getDateFormats(), this.getLocale(), this.getTimeZone()); + } + + /* Date formats. */ + + public void setDateFormats(LinkedHashSet dateFormats) { + if (null == dateFormats) { + throw new IllegalArgumentException("Invalid 'dateFormats': null."); + } + else if (0 == dateFormats.size()) { + throw new IllegalArgumentException("Invalid 'dateFormats': empty."); + } + else { + for (String dateFormat: dateFormats) { + MDateConverter.checkDateFormat(dateFormat); + } + } + // + synchronized (this.dateFormats) { + this.dateFormats = dateFormats; + } + } + + public void addDateFormat(String dateFormat) { + MDateConverter.checkDateFormat(dateFormat); + // + synchronized (this.dateFormats) { + this.dateFormats.add(dateFormat); + } + } + + public LinkedHashSet getDateFormats() { + return this.dateFormats; + } + + public String getDefaultDateFormat() { + return this.getDateFormats().iterator().next(); + } + + public LinkedHashSet getSecondaryDateFormats() { + LinkedHashSet dateFormats = (LinkedHashSet)this.getDateFormats().clone(); + // + dateFormats.remove(this.getDefaultDateFormat()); + // + return dateFormats; + } + + /* Locale. */ + + protected void setLocale(Locale locale) { + if (null == locale) { + throw new IllegalArgumentException("Invalid 'locale': null."); + } + // + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + /* Time zone. */ + + protected void setTimeZone(TimeZone timeZone) { + if (null == timeZone) { + throw new IllegalArgumentException("Invalid 'timeZone': null."); + } + // + this.timeZone = timeZone; + } + + public TimeZone getTimeZone() { + return this.timeZone; + } + + /* Conversions. */ + + protected static void checkDateFormat(String dateFormat) { + if (MText.isBlank(dateFormat)) { + throw new IllegalArgumentException("Invalid 'dateFormat': null or empty."); + } + // + try { + SimpleDateFormat testDateFormat = new SimpleDateFormat(dateFormat); + } + catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception + } + } + + protected static Date createDateFromString(String x, String inputDateFormat, Locale inputLocale, TimeZone inputTimeZone) throws MInvalidConversionFormatException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + MDateConverter.checkDateFormat(inputDateFormat); + if (null == inputLocale) { + throw new IllegalArgumentException("Invalid 'inputLocale': null."); + } + if (null == inputTimeZone) { + throw new IllegalArgumentException("Invalid 'inputTimeZone': null."); + } + // + Calendar c1 = Calendar.getInstance(inputTimeZone, inputLocale); + c1.clear(); + c1.setLenient(false); + SimpleDateFormat sdf = new SimpleDateFormat(inputDateFormat, inputLocale); + sdf.setCalendar(c1); + // + Date d1 = null; + try { + d1 = sdf.parse(x); + } + catch (ParseException exception) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception + } + // + Calendar c2 = Calendar.getInstance(inputTimeZone, inputLocale); + c2.clear(); + c2.set(Calendar.YEAR, c1.get(Calendar.YEAR)); + c2.set(Calendar.MONTH, c1.get(Calendar.MONTH)); + c2.set(Calendar.DAY_OF_MONTH, c1.get(Calendar.DAY_OF_MONTH)); + c2.set(Calendar.HOUR_OF_DAY, c1.get(Calendar.HOUR_OF_DAY)); + c2.set(Calendar.MINUTE, c1.get(Calendar.MINUTE)); + c2.set(Calendar.SECOND, c1.get(Calendar.SECOND)); + // + Date d2 = c2.getTime(); + if (!x.equals(sdf.format(d2))) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); + } + // + return d2; + } + + protected static String createStringFromDate(Date date, String outputDateFormat, Locale outputLocale, TimeZone outputTimeZone) { + if (null == date) { + throw new IllegalArgumentException("Invalid 'date': null."); + } + MDateConverter.checkDateFormat(outputDateFormat); + if (null == outputLocale) { + throw new IllegalArgumentException("Invalid 'outputLocale': null."); + } + if (null == outputTimeZone) { + throw new IllegalArgumentException("Invalid 'outputTimeZone': null."); + } + // + SimpleDateFormat sdf = new SimpleDateFormat(outputDateFormat, outputLocale); + sdf.setCalendar(Calendar.getInstance(outputTimeZone, outputLocale)); + // + return sdf.format(date); + } + + public Date createDateFromString(String x) throws MInvalidConversionFormatException { + Date y = null; + for (String dateFormat: this.getDateFormats()) { + try { + y = MDateConverter.createDateFromString(x, dateFormat, this.getLocale(), this.getTimeZone()); + // + return y; + } + catch (MInvalidConversionFormatException exception) { + } + } + if (null == y) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x)); + } + // + return y; // necessary to avoid Java compilation errors + } + + public String createStringFromDate(Date x) { + return MDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale(), this.getTimeZone()); + } + + /* Helpers. */ + + public static Date getFlatDate(Date x) { + if (null == x) { + return x; + } + // + Calendar calendar = new GregorianCalendar(); + calendar.setTime(x); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + // + return calendar.getTime(); + } + + public static Date getCeilDate(Date x) { + if (null == x) { + return x; + } + // + Calendar calendar = new GregorianCalendar(); + calendar.setTime(x); + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + // + return calendar.getTime(); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java new file mode 100644 index 0000000..e23c0b1 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +@SuppressWarnings("serial") +public class MInvalidConversionFormatException extends MConversionException { + + /* */ + + public MInvalidConversionFormatException() { + super(); + } + + public MInvalidConversionFormatException(String message) { + super(message); + } + + public MInvalidConversionFormatException(Throwable error) { + super(error); + } + + public MInvalidConversionFormatException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java new file mode 100644 index 0000000..6f06a46 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java @@ -0,0 +1,171 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.LinkedHashSet; +import java.util.Locale; + +public class MLocalDateConverter extends MObject { + + protected LinkedHashSet dateFormats = new LinkedHashSet(); + protected Locale locale = null; + + /* */ + + public MLocalDateConverter(String defaultDateFormat, Locale locale) { + super(); + // + this.addDateFormat(defaultDateFormat); + this.setLocale(locale); + } + + public MLocalDateConverter(LinkedHashSet dateFormats, Locale locale) { + super(); + // + this.setDateFormats(dateFormats); + this.setLocale(locale); + } + + @Override + public MLocalDateConverter clone() { + return new MLocalDateConverter(this.getDateFormats(), this.getLocale()); + } + + /* Date formats. */ + + public void setDateFormats(LinkedHashSet dateFormats) { + if (null == dateFormats) { + throw new IllegalArgumentException("Invalid 'dateFormats': null."); + } + else if (0 == dateFormats.size()) { + throw new IllegalArgumentException("Invalid 'dateFormats': empty."); + } + else { + for (String dateFormat: dateFormats) { + MLocalDateConverter.checkDateFormat(dateFormat); + } + } + // + synchronized (this.dateFormats) { + this.dateFormats = dateFormats; + } + } + + public void addDateFormat(String dateFormat) { + MLocalDateConverter.checkDateFormat(dateFormat); + // + synchronized (this.dateFormats) { + this.dateFormats.add(dateFormat); + } + } + + public LinkedHashSet getDateFormats() { + return this.dateFormats; + } + + public String getDefaultDateFormat() { + return this.getDateFormats().iterator().next(); + } + + public LinkedHashSet getSecondaryDateFormats() { + LinkedHashSet dateFormats = (LinkedHashSet)this.getDateFormats().clone(); + // + dateFormats.remove(this.getDefaultDateFormat()); + // + return dateFormats; + } + + /* Locale. */ + + protected void setLocale(Locale locale) { + if (null == locale) { + throw new IllegalArgumentException("Invalid 'locale': null."); + } + // + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + /* Conversions. */ + + protected static void checkDateFormat(String dateFormat) { + if (MText.isBlank(dateFormat)) { + throw new IllegalArgumentException("Invalid 'dateFormat': null or empty."); + } + // + try { + DateTimeFormatter.ofPattern(dateFormat).withResolverStyle(ResolverStyle.STRICT); + } + catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception + } + } + + protected static LocalDate createDateFromString(String x, String inputDateFormat, Locale inputLocale) throws MInvalidConversionFormatException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + MLocalDateConverter.checkDateFormat(inputDateFormat); + if (null == inputLocale) { + throw new IllegalArgumentException("Invalid 'inputLocale': null."); + } + // + LocalDate d = null; + try { + d = LocalDate.parse(x, DateTimeFormatter.ofPattern(inputDateFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT)); + } + catch (DateTimeParseException exception) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception + } + // + return d; + } + + protected static String createStringFromDate(LocalDate date, String outputDateFormat, Locale outputLocale) { + if (null == date) { + throw new IllegalArgumentException("Invalid 'date': null."); + } + MLocalDateConverter.checkDateFormat(outputDateFormat); + if (null == outputLocale) { + throw new IllegalArgumentException("Invalid 'outputLocale': null."); + } + // + return date.format(DateTimeFormatter.ofPattern(outputDateFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT)); + } + + public LocalDate createDateFromString(String x) throws MInvalidConversionFormatException { + LocalDate y = null; + for (String dateFormat: this.getDateFormats()) { + try { + y = MLocalDateConverter.createDateFromString(x, dateFormat, this.getLocale()); + // + return y; + } + catch (MInvalidConversionFormatException exception) { + } + } + if (null == y) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x)); + } + // + return y; // necessary to avoid Java compilation errors + } + + public String createStringFromDate(LocalDate x) { + return MLocalDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale()); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java new file mode 100644 index 0000000..00fae55 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java @@ -0,0 +1,171 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.util.LinkedHashSet; +import java.util.Locale; + +public class MLocalDateTimeConverter extends MObject { + + protected LinkedHashSet datetimeFormats = new LinkedHashSet(); + protected Locale locale = null; + + /* */ + + public MLocalDateTimeConverter(String defaultDatetimeFormat, Locale locale) { + super(); + // + this.addDatetimeFormat(defaultDatetimeFormat); + this.setLocale(locale); + } + + public MLocalDateTimeConverter(LinkedHashSet datetimeFormats, Locale locale) { + super(); + // + this.setDatetimeFormats(datetimeFormats); + this.setLocale(locale); + } + + @Override + public MLocalDateTimeConverter clone() { + return new MLocalDateTimeConverter(this.getDatetimeFormats(), this.getLocale()); + } + + /* Datetime formats. */ + + public void setDatetimeFormats(LinkedHashSet datetimeFormats) { + if (null == datetimeFormats) { + throw new IllegalArgumentException("Invalid 'datetimeFormats': null."); + } + else if (0 == datetimeFormats.size()) { + throw new IllegalArgumentException("Invalid 'datetimeFormats': empty."); + } + else { + for (String datetimeFormat: datetimeFormats) { + MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat); + } + } + // + synchronized (this.datetimeFormats) { + this.datetimeFormats = datetimeFormats; + } + } + + public void addDatetimeFormat(String datetimeFormat) { + MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat); + // + synchronized (this.datetimeFormats) { + this.datetimeFormats.add(datetimeFormat); + } + } + + public LinkedHashSet getDatetimeFormats() { + return this.datetimeFormats; + } + + public String getDefaultDatetimeFormat() { + return this.getDatetimeFormats().iterator().next(); + } + + public LinkedHashSet getSecondaryDatetimeFormats() { + LinkedHashSet datetimeFormats = (LinkedHashSet)this.getDatetimeFormats().clone(); + // + datetimeFormats.remove(this.getDefaultDatetimeFormat()); + // + return datetimeFormats; + } + + /* Locale. */ + + protected void setLocale(Locale locale) { + if (null == locale) { + throw new IllegalArgumentException("Invalid 'locale': null."); + } + // + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + /* Conversions. */ + + protected static void checkDatetimeFormat(String datetimeFormat) { + if (MText.isBlank(datetimeFormat)) { + throw new IllegalArgumentException("Invalid 'datetimeFormat': null or empty."); + } + // + try { + DateTimeFormatter.ofPattern(datetimeFormat).withResolverStyle(ResolverStyle.STRICT); + } + catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format("Invalid 'datetimeFormat': %s.", datetimeFormat)); // no need to propagate exception + } + } + + protected static LocalDateTime createDatetimeFromString(String x, String inputDatetimeFormat, Locale inputLocale) throws MInvalidConversionFormatException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + MLocalDateTimeConverter.checkDatetimeFormat(inputDatetimeFormat); + if (null == inputLocale) { + throw new IllegalArgumentException("Invalid 'inputLocale': null."); + } + // + LocalDateTime dt = null; + try { + dt = LocalDateTime.parse(x, DateTimeFormatter.ofPattern(inputDatetimeFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT)); + } + catch (DateTimeParseException exception) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDatetimeFormat)); // no need to propagate exception + } + // + return dt; + } + + protected static String createStringFromDatetime(LocalDateTime datetime, String outputDatetimeFormat, Locale outputLocale) { + if (null == datetime) { + throw new IllegalArgumentException("Invalid 'datetime': null."); + } + MLocalDateTimeConverter.checkDatetimeFormat(outputDatetimeFormat); + if (null == outputLocale) { + throw new IllegalArgumentException("Invalid 'outputLocale': null."); + } + // + return datetime.format(DateTimeFormatter.ofPattern(outputDatetimeFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT)); + } + + public LocalDateTime createDatetimeFromString(String x) throws MInvalidConversionFormatException { + LocalDateTime y = null; + for (String datetimeFormat: this.getDatetimeFormats()) { + try { + y = MLocalDateTimeConverter.createDatetimeFromString(x, datetimeFormat, this.getLocale()); + // + return y; + } + catch (MInvalidConversionFormatException exception) { + } + } + if (null == y) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x)); + } + // + return y; // necessary to avoid Java compilation errors + } + + public String createStringFromDatetime(LocalDateTime x) { + return MLocalDateTimeConverter.createStringFromDatetime(x, this.getDefaultDatetimeFormat(), this.getLocale()); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java b/10.x/src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java new file mode 100644 index 0000000..d599c08 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java @@ -0,0 +1,178 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.conversion; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParsePosition; +import java.util.LinkedHashSet; +import java.util.Locale; + +public class MNumberConverter extends MObject { + + protected LinkedHashSet numberFormats = new LinkedHashSet(); + protected Locale locale = null; + + /* */ + + public MNumberConverter(String defaultNumberFormat, Locale locale) { + super(); + // + this.addNumberFormat(defaultNumberFormat); + this.setLocale(locale); + } + + public MNumberConverter(LinkedHashSet numberFormats, Locale locale) { + super(); + // + this.setNumberFormats(numberFormats); + this.setLocale(locale); + } + + @Override + public MNumberConverter clone() { + return new MNumberConverter(this.getNumberFormats(), this.getLocale()); + } + + /* Number formats. */ + + public void setNumberFormats(LinkedHashSet numberFormats) { + if (null == numberFormats) { + throw new IllegalArgumentException("Invalid 'numberFormats': null."); + } + else if (0 == numberFormats.size()) { + throw new IllegalArgumentException("Invalid 'numberFormats': empty."); + } + else { + for (String numberFormat: numberFormats) { + MNumberConverter.checkNumberFormat(numberFormat); + } + } + // + synchronized (this.numberFormats) { + this.numberFormats = numberFormats; + } + } + + public void addNumberFormat(String numberFormat) { + MNumberConverter.checkNumberFormat(numberFormat); + // + synchronized (this.numberFormats) { + this.numberFormats.add(numberFormat); + } + } + + public LinkedHashSet getNumberFormats() { + return this.numberFormats; + } + + public String getDefaultNumberFormat() { + return this.getNumberFormats().iterator().next(); + } + + public LinkedHashSet getSecondaryNumberFormats() { + LinkedHashSet numberFormats = (LinkedHashSet)this.getNumberFormats().clone(); + // + numberFormats.remove(this.getDefaultNumberFormat()); + // + return numberFormats; + } + + /* Locale. */ + + protected void setLocale(Locale locale) { + if (null == locale) { + throw new IllegalArgumentException("Invalid 'locale': null."); + } + // + this.locale = locale; + } + + public Locale getLocale() { + return this.locale; + } + + /* Conversions. */ + + protected static void checkNumberFormat(String numberFormat) { + if (MText.isBlank(numberFormat)) { + throw new IllegalArgumentException("Invalid 'numberFormat': null or empty."); + } + // + try { + DecimalFormat testNumberFormat = new DecimalFormat(numberFormat); + } + catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format("Invalid 'numberFormat': %s.", numberFormat)); // no need to propagate exception + } + } + + protected static BigDecimal createNumberFromString(String x, String inputNumberFormat, Locale inputLocale) throws MInvalidConversionFormatException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + MNumberConverter.checkNumberFormat(inputNumberFormat); + if (null == inputLocale) { + throw new IllegalArgumentException("Invalid 'inputLocale': null."); + } + // + DecimalFormatSymbols dfs = new DecimalFormatSymbols(inputLocale); + DecimalFormat df = new DecimalFormat(inputNumberFormat, dfs); + df.setParseBigDecimal(true); + // + ParsePosition validPosition = new ParsePosition(0); + BigDecimal bd = (BigDecimal)df.parse(x, validPosition); + if (validPosition.getIndex() < x.length()) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputNumberFormat)); + } + // + return bd; + } + + protected static String createStringFromNumber(BigDecimal number, String outputNumberFormat, Locale outputLocale) { + if (null == number) { + throw new IllegalArgumentException("Invalid 'number': null."); + } + MNumberConverter.checkNumberFormat(outputNumberFormat); + if (null == outputLocale) { + throw new IllegalArgumentException("Invalid 'outputLocale': null."); + } + // + DecimalFormatSymbols dfs = new DecimalFormatSymbols(outputLocale); + DecimalFormat df = new DecimalFormat(outputNumberFormat, dfs); + df.setRoundingMode(RoundingMode.HALF_UP); + // + return df.format(number); + } + + public BigDecimal createNumberFromString(String x) throws MInvalidConversionFormatException { + BigDecimal y = null; + for (String numberFormat: this.getNumberFormats()) { + try { + y = MNumberConverter.createNumberFromString(x, numberFormat, this.getLocale()); + // + return y; + } + catch (MInvalidConversionFormatException exception) { + } + } + if (null == y) { + throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x)); + } + // + return y; // necessary to avoid Java compilation errors + } + + public String createStringFromNumber(BigDecimal x) { + return MNumberConverter.createStringFromNumber(x, this.getDefaultNumberFormat(), this.getLocale()); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnection.java b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnection.java new file mode 100644 index 0000000..8e8dd54 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnection.java @@ -0,0 +1,346 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.logging.MLogListener; +import com.marcozanon.macaco.text.MText; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.LinkedList; + +public class MDatabaseConnection extends MObject { + + public static enum TransactionStatus { + CLOSED, + SUCCESSFUL, + FAILED + }; + + protected String driver = null; + protected String url = null; + protected String username = null; + protected String password = null; + protected boolean localTypesMode = false; + protected MLogListener logListener = null; + + protected Connection connection = null; + + protected MDatabaseConnection.TransactionStatus transactionStatus = MDatabaseConnection.TransactionStatus.CLOSED; + + /* */ + + public MDatabaseConnection(String driver, String url, String username, String password, boolean localTypesMode, MLogListener logListener) throws MDatabaseConnectionFailureException { + super(); + // + if (MText.isBlank(driver)) { + throw new IllegalArgumentException("Invalid 'driver': null or empty."); + } + if (MText.isBlank(url)) { + throw new IllegalArgumentException("Invalid 'url': null or empty."); + } + if (null == username) { + throw new IllegalArgumentException("Invalid 'username': null."); + } + if (null == password) { + throw new IllegalArgumentException("Invalid 'password': null."); + } + // + this.driver = driver; + this.url = url; + this.username = username; + this.password = password; + this.localTypesMode = localTypesMode; + this.setLogListener(logListener); + // Load driver. + try { + Class.forName(this.getDriver()).newInstance(); + } + catch (ClassNotFoundException exception) { + throw new MDatabaseConnectionFailureException("Could not load driver.", exception); + } + catch (IllegalAccessException exception) { + throw new MDatabaseConnectionFailureException("Could not load driver.", exception); + } + catch (InstantiationException exception) { + throw new MDatabaseConnectionFailureException("Could not load driver.", exception); + } + // Initialize the connection. + this.initialize(); + } + + public void initialize() throws MDatabaseConnectionFailureException { + // Establish a connection. + try { + this.connection = DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()); + } + catch (SQLException exception) { + throw new MDatabaseConnectionFailureException("Could not get database connection.", exception); + } + // Set SQL mode to standard ANSI and TRADITIONAL. + try { + this.getConnection().createStatement().executeUpdate("SET SQL_MODE = 'ANSI,TRADITIONAL'"); +/* Disabled to prevent an infinite loop. + this.logStatement("### SET SQL_MODE = 'ANSI,TRADITIONAL' ###"); +*/ + } + catch (SQLException exception) { + throw new MDatabaseConnectionFailureException("Could not set SQL mode to ANSI and TRADITIONAL.", exception); + } + } + + protected void check() throws MDatabaseConnectionFailureException { + try { + this.getConnection().createStatement().executeQuery("/* ping */ SELECT 1"); + } + catch (SQLException exception) { + this.initialize(); + } + } + + public void close() throws MDatabaseConnectionFailureException { + try { + this.getConnection().close(); + } + catch (SQLException exception) { + throw new MDatabaseConnectionFailureException("Could not close database connection.", exception); + } + } + + public boolean isClosed() throws MDatabaseConnectionFailureException { + try { + return this.getConnection().isClosed(); + } + catch (SQLException exception) { + throw new MDatabaseConnectionFailureException("Could not determine the state.", exception); + } + } + + @Override + protected void finalize() { + try { + this.close(); + } + catch (Exception exception) { + } + } + + /* Driver. */ + + public String getDriver() { + return this.driver; + } + + /* Url. */ + + public String getUrl() { + return this.url; + } + + /* Username. */ + + public String getUsername() { + return this.username; + } + + /* Password. */ + + public String getPassword() { + return this.password; + } + + /* Local types mode. */ + + public boolean getLocalTypesMode() { + return this.localTypesMode; + } + + /* Connection. */ + + protected Connection getConnection() { + return this.connection; + } + + /* Transactions. */ + + protected void setTransactionStatus(MDatabaseConnection.TransactionStatus transactionStatus) { + if (null == transactionStatus) { + throw new IllegalArgumentException("Invalid 'transactionStatus': null."); + } + // + this.transactionStatus = transactionStatus; + } + + public MDatabaseConnection.TransactionStatus getTransactionStatus() { + return this.transactionStatus; + } + + public void startTransaction() throws MSqlTransactionException { + if (MDatabaseConnection.TransactionStatus.CLOSED != this.getTransactionStatus()) { + throw new MSqlTransactionException("Nested transactions not allowed."); + } + // + try { + // Check the connection. + this.check(); + // Start the transaction. + this.getConnection().setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + this.getConnection().setAutoCommit(false); + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.SUCCESSFUL); + this.logStatement("### BEGIN TRANSACTION ###"); + } + catch (MDatabaseConnectionFailureException exception) { + throw new MSqlTransactionException("Could not start transaction.", exception); + } + catch (SQLException exception) { + throw new MSqlTransactionException("Could not start transaction.", exception); + } + } + + public void rollBackTransaction() throws MSqlTransactionException { + if (MDatabaseConnection.TransactionStatus.CLOSED == this.getTransactionStatus()) { + throw new MSqlTransactionException("Transaction not started."); + } + // + try { +/* + // Check the connection. + this.check(); +*/ + // Cancel the transaction. + this.getConnection().rollback(); + this.getConnection().setAutoCommit(true); + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.CLOSED); + this.logStatement("### ROLLBACK ###"); + } + catch (SQLException exception) { + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.FAILED); + throw new MSqlTransactionException("Could not roll back transaction.", exception); + } + } + + public MDatabaseConnection.TransactionStatus commitTransaction() throws MSqlTransactionException { + switch (this.getTransactionStatus()) { + case SUCCESSFUL: + try { +/* + // Check the connection. + this.check(); +*/ + // Commit the transaction. + this.getConnection().commit(); + this.getConnection().setAutoCommit(true); + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.CLOSED); + this.logStatement("### COMMIT ###"); + // + return MDatabaseConnection.TransactionStatus.SUCCESSFUL; + } + catch (SQLException exception) { + this.rollBackTransaction(); + // + return MDatabaseConnection.TransactionStatus.FAILED; + } + case FAILED: + this.rollBackTransaction(); + // + return MDatabaseConnection.TransactionStatus.FAILED; + default: // instead of "case CLOSED:" (necessary to avoid Java compilation errors) + throw new MSqlTransactionException("Transaction not started."); + } + } + + /* Statements. */ + + public MSqlStatementResults executePreparedStatement(String statement) throws MDatabaseConnectionFailureException, MSqlStatementException { + return this.executePreparedStatement(statement, new LinkedList()); + } + + public MSqlStatementResults executePreparedStatement(String statement, LinkedList parameters) throws MDatabaseConnectionFailureException, MSqlStatementException { + return this.executePreparedStatement(statement, parameters, this.getLocalTypesMode()); + } + + public MSqlStatementResults executePreparedStatement(String statement, LinkedList parameters, boolean localTypesMode) throws MDatabaseConnectionFailureException, MSqlStatementException { + return this.executePreparedStatement(statement, parameters, this.getLocalTypesMode(), true); + }; + + public MSqlStatementResults executePreparedStatement(String statement, LinkedList parameters, boolean localTypesMode, boolean loggableStatement) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (MText.isBlank(statement)) { + throw new IllegalArgumentException("Invalid 'statement': null or empty."); + } + if (null == parameters) { + throw new IllegalArgumentException("Invalid 'parameters': null."); + } + // + MSqlStatementResults results = null; + PreparedStatement preparedStatement = null; + try { + // Check the connection. + this.check(); + // Prepare the statement. + preparedStatement = this.getConnection().prepareStatement(statement, PreparedStatement.RETURN_GENERATED_KEYS); + for (int p = 0; parameters.size() > p; p++) { + preparedStatement.setObject(p + 1, parameters.get(p)); + } + // Execute the statement and parse the results. + preparedStatement.execute(); + results = new MSqlStatementResults(preparedStatement, localTypesMode); + if (loggableStatement) { + this.logStatement(preparedStatement.toString()); + } + } + catch (MDatabaseConnectionFailureException exception) { + if (MDatabaseConnection.TransactionStatus.SUCCESSFUL == this.getTransactionStatus()) { + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.FAILED); + } + throw new MSqlStatementException(String.format("Could not execute prepared statement: %s.", preparedStatement), exception); + } + catch (SQLException exception) { + if (MDatabaseConnection.TransactionStatus.SUCCESSFUL == this.getTransactionStatus()) { + this.setTransactionStatus(MDatabaseConnection.TransactionStatus.FAILED); + } + throw new MSqlStatementException(String.format("Could not execute prepared statement: %s.", preparedStatement), exception); + } + // + return results; + } + + /* Tables. */ + + public MSqlTable getTable(String table, String primaryKey) { + return new MSqlTable(this, table, primaryKey); + } + + /* Logging. */ + + public void setLogListener(MLogListener logListener) { + this.logListener = logListener; + } + + protected MLogListener getLogListener() { + return this.logListener; + } + + protected void logStatement(String statement) { + MLogListener logListener = this.getLogListener(); + if (null != logListener) { + logListener.onMessageLogging(statement); + } + } + + /* Engine version. */ + + public String getEngineVersion() throws MDatabaseConnectionFailureException, MSqlStatementException { + MSqlStatementResults results = this.executePreparedStatement("SELECT VERSION()"); + LinkedList> resultList = results.getRecords(); + // + return (String)resultList.get(0).get("VERSION()"); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionFailureException.java b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionFailureException.java new file mode 100644 index 0000000..c8fdda3 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionFailureException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +@SuppressWarnings("serial") +public class MDatabaseConnectionFailureException extends MDatabaseException { + + /* */ + + public MDatabaseConnectionFailureException() { + super(); + } + + public MDatabaseConnectionFailureException(String message) { + super(message); + } + + public MDatabaseConnectionFailureException(Throwable error) { + super(error); + } + + public MDatabaseConnectionFailureException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionGenerator.java b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionGenerator.java new file mode 100644 index 0000000..7ea9348 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionGenerator.java @@ -0,0 +1,121 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.logging.MLogListener; +import com.marcozanon.macaco.text.MText; + +public class MDatabaseConnectionGenerator extends MObject { + + protected String driver = null; + protected String url = null; + protected String username = null; + protected String password = null; + protected boolean localTypesMode = false; + protected MLogListener logListener = null; + + /* */ + + public MDatabaseConnectionGenerator(String driver, String url, String username, String password, boolean localTypesMode, MLogListener logListener) { + super(); + // + if (MText.isBlank(driver)) { + throw new IllegalArgumentException("Invalid 'driver': null or empty."); + } + if (MText.isBlank(url)) { + throw new IllegalArgumentException("Invalid 'url': null or empty."); + } + if (null == username) { + throw new IllegalArgumentException("Invalid 'username': null."); + } + if (null == password) { + throw new IllegalArgumentException("Invalid 'password': null."); + } + // + this.driver = driver; + this.url = url; + this.username = username; + this.password = password; + this.localTypesMode = localTypesMode; + this.setLogListener(logListener); + } + + /* Driver. */ + + public String getDriver() { + return this.driver; + } + + /* Url. */ + + public String getUrl() { + return this.url; + } + + /* Username. */ + + public String getUsername() { + return this.username; + } + + /* Password. */ + + public String getPassword() { + return this.password; + } + + /* Local types mode. */ + + public boolean getLocalTypesMode() { + return this.localTypesMode; + } + + /* Log listener. */ + + public MLogListener getLogListener() { + return this.logListener; + } + + public void setLogListener(MLogListener logListener) { + this.logListener = logListener; + } + + /* Generator. */ + + public MDatabaseConnection getNewDatabaseConnection() throws MDatabaseConnectionFailureException { + return new MDatabaseConnection(this.getDriver(), this.getUrl(), this.getUsername(), this.getPassword(), this.getLocalTypesMode(), this.getLogListener()); + } + + public boolean isGeneratorFor(MDatabaseConnection databaseConnection) { + if (null == databaseConnection) { + throw new IllegalArgumentException("Invalid 'databaseConnection': null."); + } + // + if (!databaseConnection.getDriver().equals(this.getDriver())) { + return false; + } + if (!databaseConnection.getUrl().equals(this.getUrl())) { + return false; + } + if (!databaseConnection.getUsername().equals(this.getUsername())) { + return false; + } + if (!databaseConnection.getPassword().equals(this.getPassword())) { + return false; + } + if (databaseConnection.getLocalTypesMode() != this.getLocalTypesMode()) { + return false; + } + if (databaseConnection.getLogListener() != this.getLogListener()) { + return false; + } + // + return true; + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionPool.java b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionPool.java new file mode 100644 index 0000000..b40e934 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseConnectionPool.java @@ -0,0 +1,136 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.logging.MLogListener; +import java.util.LinkedList; + +public class MDatabaseConnectionPool extends MObject { + + protected MDatabaseConnectionGenerator databaseConnectionGenerator = null; + + protected LinkedList databaseConnections = new LinkedList(); + protected int minimumSize = 0; + protected int maximumSize = 0; + + /* */ + + public MDatabaseConnectionPool(String driver, String url, String username, String password, boolean localTypesMode, int minimumSize, int maximumSize, MLogListener logListener) throws MDatabaseConnectionFailureException { + super(); + // + if (0 > minimumSize) { + throw new IllegalArgumentException(String.format("Invalid 'minimumSize': %s.", minimumSize)); + } + if (1 > maximumSize) { + throw new IllegalArgumentException(String.format("Invalid 'maximumSize': %s.", maximumSize)); + } + else if (minimumSize > maximumSize) { + throw new IllegalArgumentException(String.format("Invalid 'maximumSize': %s < %s ('minimumSize').", maximumSize, minimumSize)); + } + // + this.databaseConnectionGenerator = new MDatabaseConnectionGenerator(driver, url, username, password, localTypesMode, logListener); + this.minimumSize = minimumSize; + this.maximumSize = maximumSize; + // + this.initialize(); + } + + protected void initialize() throws MDatabaseConnectionFailureException { + for (int c = 0; c < this.getMinimumSize(); c++) { + MDatabaseConnection databaseConnection = this.getDatabaseConnectionGenerator().getNewDatabaseConnection(); + this.pushDatabaseConnection(databaseConnection); + } + } + + protected void closeConnections() { + LinkedList databaseConnections = this.getDatabaseConnections(); + while (0 < databaseConnections.size()) { + MDatabaseConnection databaseConnection = databaseConnections.removeLast(); + // + try { + databaseConnection.close(); + } + catch (MDatabaseConnectionFailureException exception) { + } + } + } + + @Override + protected void finalize() { + this.closeConnections(); + } + + /* Database connection generator. */ + + protected MDatabaseConnectionGenerator getDatabaseConnectionGenerator() { + return this.databaseConnectionGenerator; + } + + /* Database connections. */ + + protected LinkedList getDatabaseConnections() { + return this.databaseConnections; + } + + protected int getMinimumSize() { + return this.minimumSize; + } + + protected int getMaximumSize() { + return this.maximumSize; + } + + public synchronized MDatabaseConnection popDatabaseConnection() throws MDatabaseConnectionFailureException { + LinkedList databaseConnections = this.getDatabaseConnections(); + // + MDatabaseConnection databaseConnection = null; + if (0 == databaseConnections.size()) { + databaseConnection = this.getDatabaseConnectionGenerator().getNewDatabaseConnection(); + } + else { + databaseConnection = databaseConnections.removeLast(); + } + // + if (this.getMinimumSize() > databaseConnections.size()) { + databaseConnections.add(this.getDatabaseConnectionGenerator().getNewDatabaseConnection()); + } + // + return databaseConnection; + } + + public synchronized void pushDatabaseConnection(MDatabaseConnection databaseConnection) throws MDatabaseConnectionFailureException { + if (null == databaseConnection) { + throw new IllegalArgumentException("Invalid 'databaseConnection': null."); + } + else if (databaseConnection.isClosed()) { + throw new IllegalArgumentException("Invalid 'databaseConnection': closed."); + } + // + if (!this.getDatabaseConnectionGenerator().isGeneratorFor(databaseConnection)) { + databaseConnection.close(); + } + else { + LinkedList databaseConnections = this.getDatabaseConnections(); + // + if (this.getMaximumSize() >= databaseConnections.size()) { + databaseConnections.add(databaseConnection); + } + } + } + + /* Logging. */ + + public synchronized void setLogListener(MLogListener logListener) { + this.getDatabaseConnectionGenerator().setLogListener(logListener); + // + for (MDatabaseConnection databaseConnection: this.getDatabaseConnections()) { + databaseConnection.setLogListener(logListener); + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseException.java b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseException.java new file mode 100644 index 0000000..498a752 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MDatabaseException.java @@ -0,0 +1,31 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MException; + +public abstract class MDatabaseException extends MException { + + /* */ + + public MDatabaseException() { + super(); + } + + public MDatabaseException(String message) { + super(message); + } + + public MDatabaseException(Throwable error) { + super(error); + } + + public MDatabaseException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementException.java b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementException.java new file mode 100644 index 0000000..cd67820 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +@SuppressWarnings("serial") +public class MSqlStatementException extends MDatabaseException { + + /* */ + + public MSqlStatementException() { + super(); + } + + public MSqlStatementException(String message) { + super(message); + } + + public MSqlStatementException(Throwable error) { + super(error); + } + + public MSqlStatementException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementResults.java b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementResults.java new file mode 100644 index 0000000..d4c9632 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlStatementResults.java @@ -0,0 +1,150 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; + +public class MSqlStatementResults extends MObject { + + protected PreparedStatement preparedStatement = null; + protected boolean localTypesMode = false; + + protected ResultSet resultSet = null; + + protected LinkedList> records = new LinkedList>(); + + protected LinkedHashSet generatedKeys = new LinkedHashSet(); + + /* */ + + public MSqlStatementResults(PreparedStatement preparedStatement, boolean localTypesMode) throws MSqlStatementException { + super(); + // + if (null == preparedStatement) { + throw new IllegalArgumentException("Invalid 'preparedStatement': null."); + } + // + this.preparedStatement = preparedStatement; + this.localTypesMode = localTypesMode; + // + try { + this.resultSet = this.getPreparedStatement().getResultSet(); + if (null != this.getResultSet()) { + while (this.getResultSet().next()) { + ResultSetMetaData resultSetMetaData = this.getResultSet().getMetaData(); + LinkedHashMap record = new LinkedHashMap(); + // + for (int f = 0; resultSetMetaData.getColumnCount() > f; f++) { + String field = resultSetMetaData.getColumnLabel(f + 1); + Object value = this.getResultSet().getObject(field); + // + if ((value instanceof java.sql.Date) && (this.getLocalTypesMode())) { + value = ((java.sql.Date)value).toLocalDate(); + } + else if ((value instanceof java.sql.Time) && (this.getLocalTypesMode())) { + value = ((java.sql.Time)value).toLocalTime(); + } + else if ((value instanceof java.sql.Timestamp) && (this.getLocalTypesMode())) { + value = ((java.sql.Timestamp)value).toLocalDateTime(); + } + // + record.put(field, value); + } + // + this.getRecords().add(record); + } + } + } + catch (SQLException exception) { + throw new MSqlStatementException("Could not retrieve statement records.", exception); + } + // + try { + ResultSet generatedKeysResultSet = this.getPreparedStatement().getGeneratedKeys(); + while (generatedKeysResultSet.next()) { + this.getGeneratedKeys().add(generatedKeysResultSet.getObject(1)); + } + generatedKeysResultSet.close(); + } + catch (SQLException exception) { + throw new MSqlStatementException("Could not retrieve generated keys.", exception); + } + } + + public void close() throws MDatabaseConnectionFailureException { + try { + if (null != this.getResultSet()) { + this.getResultSet().close(); + } + this.getPreparedStatement().close(); + } + catch (SQLException exception) { + throw new MDatabaseConnectionFailureException("Could not close statement and/or result set references.", exception); + } + } + + @Override + protected void finalize() { + try { + this.close(); + } + catch (Exception exception) { + } + } + + /* References. */ + + protected PreparedStatement getPreparedStatement() { + return this.preparedStatement; + } + + public ResultSet getResultSet() { + return this.resultSet; + } + + public boolean getLocalTypesMode() { + return this.localTypesMode; + } + + /* Records. */ + + public LinkedList> getRecords() { + return this.records; + } + + public LinkedList getRecordsByField(String field) { + if (MText.isBlank(field)) { + throw new IllegalArgumentException("Invalid 'field': null or empty."); + } + // + LinkedList recordsByField = new LinkedList(); + for (LinkedHashMap record: this.getRecords()) { + Object r = record.get(field); + if (null == r) { + throw new IllegalArgumentException(String.format("Invalid 'field': %s.", field)); + } + recordsByField.add(r); + } + // + return recordsByField; + } + + /* Generated keys. */ + + public LinkedHashSet getGeneratedKeys() { + return this.generatedKeys; + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTable.java b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTable.java new file mode 100644 index 0000000..d58d8e4 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTable.java @@ -0,0 +1,177 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +import com.marcozanon.macaco.MObject; +import com.marcozanon.macaco.text.MText; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.StringJoiner; + +public class MSqlTable extends MObject { + + protected MDatabaseConnection databaseConnection = null; + protected String table = null; + protected String primaryKey = null; + + /* */ + + public MSqlTable(MDatabaseConnection databaseConnection, String table, String primaryKey) { + super(); + // + if (null == databaseConnection) { + throw new IllegalArgumentException("Invalid 'databaseConnection': null."); + } + if (MText.isBlank(table)) { + throw new IllegalArgumentException("Invalid 'table': null or empty."); + } + if (MText.isBlank(primaryKey)) { + throw new IllegalArgumentException("Invalid 'primaryKey': null or empty."); + } + // + this.databaseConnection = databaseConnection; + this.table = table; + this.primaryKey = primaryKey; + } + + /* Database connection. */ + + protected MDatabaseConnection getDatabaseConnection() { + return this.databaseConnection; + } + + /* Table. */ + + public String getTable() { + return this.table; + } + + /* Primary key. */ + + public String getPrimaryKey() { + return this.primaryKey; + } + + /* Statements. */ + + protected MSqlStatementResults insertRecord(LinkedHashMap map, boolean loggableStatement) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == map) { + throw new IllegalArgumentException("Invalid 'map': null."); + } + // + LinkedList p = new LinkedList(); + StringBuilder fields = new StringBuilder(""); + StringBuilder s = new StringBuilder(""); + // + for (String field: map.keySet()) { + fields.append("\"" + this.getTable() + "\".\"" + field + "\", "); + s.append("?, "); + p.add(map.get(field)); + } + fields.delete(fields.length() - 2, fields.length()); + s.delete(s.length() - 2, s.length()); + // + return this.getDatabaseConnection().executePreparedStatement(" INSERT INTO \"" + this.getTable() + "\"" + + " (" + fields + ")" + + " VALUES (" + s + ")", + p, true, loggableStatement); + } + + protected MSqlStatementResults updateRecord(LinkedHashMap map, Object id, boolean loggableStatement) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == map) { + throw new IllegalArgumentException("Invalid 'map': null."); + } + if (null == id) { + throw new IllegalArgumentException("Invalid 'id': null."); + } + // + LinkedList p = new LinkedList(); + StringBuilder s = new StringBuilder(""); + // + for (String field: map.keySet()) { + s.append("\"" + this.getTable() + "\".\"" + field + "\" = ?, "); + p.add(map.get(field)); + } + s.delete(s.length() - 2, s.length()); + p.add(id); + // + return this.getDatabaseConnection().executePreparedStatement(" UPDATE \"" + this.getTable() + "\"" + + " SET " + s + + " WHERE (\"" + this.getTable() + "\".\"" + this.getPrimaryKey() + "\" = ?)", + p, true, loggableStatement); + } + + public MSqlStatementResults setRecord(LinkedHashMap map, Object id) throws MDatabaseConnectionFailureException, MSqlStatementException { + return this.setRecord(map, id, true); + } + + public MSqlStatementResults setRecord(LinkedHashMap map, Object id, boolean loggableStatement) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == id) { + return this.insertRecord(map, loggableStatement); + } + else { + return this.updateRecord(map, id, loggableStatement); + } + } + + public MSqlStatementResults getRecord(Object id) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == id) { + throw new IllegalArgumentException("Invalid 'id': null."); + } + // + LinkedList p = new LinkedList(); + p.add(id); + // + return this.getDatabaseConnection().executePreparedStatement(" SELECT *" + + " FROM \"" + this.getTable() + "\"" + + " WHERE (\"" + this.getTable() + "\".\"" + this.getPrimaryKey() + "\" = ?)", + p); + } + + public MSqlStatementResults deleteRecord(Object id) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == id) { + throw new IllegalArgumentException("Invalid 'id': null."); + } + // + LinkedList p = new LinkedList(); + p.add(id); + // + return this.getDatabaseConnection().executePreparedStatement(" DELETE FROM \"" + this.getTable() + "\"" + + " WHERE (\"" + this.getTable() + "\".\"" + this.getPrimaryKey() + "\" = ?)", + p); + } + + public MSqlStatementResults deleteRecords(LinkedHashSet idSet) throws MDatabaseConnectionFailureException, MSqlStatementException { + if (null == idSet) { + throw new IllegalArgumentException("Invalid 'idSet': null."); + } + // + String whereClause = null; + LinkedList p = null; + // + if (0 == idSet.size()) { + whereClause = "0"; + } + else { + StringJoiner s = new StringJoiner(", "); + p = new LinkedList(); + // + for (Object id: idSet) { + s.add("?"); + p.add(id); + } + // + whereClause = "\"" + this.getTable() + "\".\"" + this.getPrimaryKey() + "\" IN (" + s.toString() + ")"; + } + // + return this.getDatabaseConnection().executePreparedStatement(" DELETE FROM \"" + this.getTable() + "\"" + + " WHERE (" + whereClause + ")", + p); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTransactionException.java b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTransactionException.java new file mode 100644 index 0000000..d63591b --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/database/MSqlTransactionException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.database; + +@SuppressWarnings("serial") +public class MSqlTransactionException extends MDatabaseException { + + /* */ + + public MSqlTransactionException() { + super(); + } + + public MSqlTransactionException(String message) { + super(message); + } + + public MSqlTransactionException(Throwable error) { + super(error); + } + + public MSqlTransactionException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MInvalidJsonValueException.java b/10.x/src/main/java/com/marcozanon/macaco/json/MInvalidJsonValueException.java new file mode 100644 index 0000000..b1fbaee --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MInvalidJsonValueException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +@SuppressWarnings("serial") +public class MInvalidJsonValueException extends MJsonException { + + /* */ + + public MInvalidJsonValueException() { + super(); + } + + public MInvalidJsonValueException(String message) { + super(message); + } + + public MInvalidJsonValueException(Throwable error) { + super(error); + } + + public MInvalidJsonValueException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonArray.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonArray.java new file mode 100644 index 0000000..9a9c4a7 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonArray.java @@ -0,0 +1,252 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.text.MText; +import java.util.LinkedList; + +public class MJsonArray extends MJsonValue { + + protected static enum ParsingMode { + START, + POST_VALUE, + POST_VALUE_SEPARATOR, + ERROR + }; + + protected LinkedList values = new LinkedList(); + + /* */ + + public MJsonArray() throws MInvalidJsonValueException { + this("[]"); + } + + public MJsonArray(String x) throws MInvalidJsonValueException { + super(); + // + this.parseString(x); + } + + @Override + public MJsonArray clone() { + MJsonArray tmpMJsonArray = null; + // + try { + tmpMJsonArray = new MJsonArray(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonArray; + } + + /* Value handlers. */ + + public void addValue(MJsonValue x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.getValues().add(x); + } + + public void setValue(int index, MJsonValue x) { + if ((0 > index) || ((this.getValueCount() - 1) < index)) { + throw new IllegalArgumentException(String.format("Invalid 'index': %s: out of range.", index)); + } + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.getValues().set(index, x); + } + + protected LinkedList getValues() { + return this.values; + } + + public MJsonValue getValue(int index) { + if ((0 > index) || ((this.getValueCount() - 1) < index)) { + throw new IllegalArgumentException(String.format("Invalid 'index': %s: out of range.", index)); + } + // + return this.getValues().get(index).clone(); + } + + public int getValueCount() { + return this.getValues().size(); + } + + public void removeValue(int index) { + if ((0 > index) || ((this.getValueCount() - 1) < index)) { + throw new IllegalArgumentException(String.format("Invalid 'index': %s: out of range.", index)); + } + // + this.getValues().remove(index); + } + + public void clearValues() { + this.getValues().clear(); + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if ((2 > x.length()) || ('[' != x.charAt(0))) { + return 0; + } + // + int position = x.indexOf("]", 1); + while (-1 < position) { + try { + MJsonArray testValue = new MJsonArray(); + testValue.parseString(x.substring(0, position + 1)); + // + return position + 1; + } + catch (MInvalidJsonValueException exception) { + position = x.indexOf("]", position + 1); + } + } + // + return 0; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + if ((2 > x.length()) || ('[' != x.charAt(0)) || (']' != x.charAt(x.length() - 1))) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: must begin and end with square brackets.", x)); + } + // + String y = x.substring(1, x.length() - 1); + int parsingPosition = 0; + MJsonArray.ParsingMode parsingMode = MJsonArray.ParsingMode.START; + while ((y.length() > parsingPosition) && (MJsonArray.ParsingMode.ERROR != parsingMode)) { + int c = y.codePointAt(parsingPosition); + switch (parsingMode) { + case START: + case POST_VALUE_SEPARATOR: // a value is expected + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if ((MJsonValue.isValueSeparator(c)) || (MJsonValue.isNameSeparator(c)) || (MJsonValue.isClosingStructuralCharacter(c))) { + parsingMode = MJsonArray.ParsingMode.ERROR; + } + else { + int offset = 0; + if (0 < (offset = MJsonNull.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonNull(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonBoolean.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonBoolean(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonNumber.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonNumber(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonString.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonString(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonArray.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonArray(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonObject.getTokenLength(y.substring(parsingPosition)))) { + this.addValue(new MJsonObject(y.substring(parsingPosition, parsingPosition + offset))); + } + if (0 < offset) { + parsingPosition += offset; + parsingMode = MJsonArray.ParsingMode.POST_VALUE; + } + else { + parsingMode = MJsonArray.ParsingMode.ERROR; + } + } + break; + case POST_VALUE: + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if (MJsonValue.isValueSeparator(c)) { + parsingPosition++; + parsingMode = MJsonArray.ParsingMode.POST_VALUE_SEPARATOR; + } + else { + parsingMode = MJsonArray.ParsingMode.ERROR; + } + break; + case ERROR: // malformed string + break; + } + } + // + if (MJsonArray.ParsingMode.POST_VALUE_SEPARATOR == parsingMode) { + parsingMode = MJsonArray.ParsingMode.ERROR; + } + if (MJsonArray.ParsingMode.ERROR == parsingMode) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json array.", x)); + } + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + StringBuilder s = new StringBuilder(""); + // + s.append("["); + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + } + for (int i = 0; this.getValueCount() > i; i++) { + if (0 < i) { + s.append(","); + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + } + else { + s.append(" "); + } + } + if (prettyPrintMode) { + for (int l = 0; (leftMargin + MConstants.JSON_PRETTY_PRINT_MODE_INDENTATION) > l; l++) { + s.append(" "); + } + } + s.append(this.getValues().get(i).getJsonValue(prettyPrintMode, leftMargin + MConstants.JSON_PRETTY_PRINT_MODE_INDENTATION)); + } + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + for (int l = 0; leftMargin > l; l++) { + s.append(" "); + } + } + s.append("]"); + // + return s.toString(); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonBoolean.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonBoolean.java new file mode 100644 index 0000000..8fd8490 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonBoolean.java @@ -0,0 +1,101 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.text.MText; + +public class MJsonBoolean extends MJsonValue { + + protected Boolean value = null; + + /* */ + + public MJsonBoolean() throws MInvalidJsonValueException { + this("false"); + } + + public MJsonBoolean(String x) throws MInvalidJsonValueException { + super(); + // + this.parseString(x); + } + + @Override + public MJsonBoolean clone() { + MJsonBoolean tmpMJsonBoolean = null; + // + try { + tmpMJsonBoolean = new MJsonBoolean(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonBoolean; + } + + /* Value handlers. */ + + public void setValue(Boolean x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.value = x; + } + + public Boolean getValue() { + return this.value; + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if (x.startsWith("true")) { + return "true".length(); + } + else if (x.startsWith("false")) { + return "false".length(); + } + // + return 0; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + if ((!"true".equals(x)) && (!"false".equals(x))) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json boolean.", x)); + } + // + this.setValue(Boolean.valueOf(x)); + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + return this.getValue().toString(); + } +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonException.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonException.java new file mode 100644 index 0000000..671b363 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonException.java @@ -0,0 +1,31 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.MException; + +public abstract class MJsonException extends MException { + + /* */ + + public MJsonException() { + super(); + } + + public MJsonException(String message) { + super(message); + } + + public MJsonException(Throwable error) { + super(error); + } + + public MJsonException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNull.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNull.java new file mode 100644 index 0000000..30f959c --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNull.java @@ -0,0 +1,80 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.text.MText; + +public class MJsonNull extends MJsonValue { + + /* */ + + public MJsonNull() throws MInvalidJsonValueException { + this("null"); + } + + public MJsonNull(String x) throws MInvalidJsonValueException { + super(); + // + this.parseString(x); + } + + @Override + public MJsonNull clone() { + MJsonNull tmpMJsonNull = null; + // + try { + tmpMJsonNull = new MJsonNull(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonNull; + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if (x.startsWith("null")) { + return "null".length(); + } + // + return 0; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + if (!"null".equals(x)) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json null.", x)); + } + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + return "null"; + } +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNumber.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNumber.java new file mode 100644 index 0000000..f281487 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonNumber.java @@ -0,0 +1,124 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.text.MText; +import java.math.BigDecimal; + +public class MJsonNumber extends MJsonValue { + + protected BigDecimal value = null; + + /* */ + + public MJsonNumber() throws MInvalidJsonValueException { + this("0"); + } + + public MJsonNumber(String x) throws MInvalidJsonValueException { + super(); + // + this.parseString(x); + } + + @Override + public MJsonNumber clone() { + MJsonNumber tmpMJsonNumber = null; + // + try { + tmpMJsonNumber = new MJsonNumber(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonNumber; + } + + /* Value handlers. */ + + public void setValue(Number x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.value = new BigDecimal(x.toString()); + } + + public BigDecimal getValue() { + return this.value; + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if (' ' == x.charAt(0)) { + return 0; + } + // + int position = 0; + int validPosition = 0; + BigDecimal y = null; + // + while (x.length() > position) { + if (' ' == x.charAt(position)) { + break; + } + else { + try { + y = new BigDecimal(x.substring(0, position + 1)); + position++; + validPosition = position; + } + catch (NumberFormatException exception) { + position++; + } + } + } + // + return validPosition; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + BigDecimal y = null; + // + try { + y = new BigDecimal(x); + } + catch (NumberFormatException exception) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json number.", x)); // no need to propagate exception + } + // + this.setValue(y); + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + return this.getValue().toString(); + } +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonObject.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonObject.java new file mode 100644 index 0000000..78bb4fd --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonObject.java @@ -0,0 +1,309 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.text.MText; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; + +public class MJsonObject extends MJsonValue { + + protected static enum ParsingMode { + START, + POST_NAME, + POST_NAME_SEPARATOR, + POST_VALUE, + POST_VALUE_SEPARATOR, + ERROR + }; + + protected LinkedHashMap values = new LinkedHashMap(); + + /* */ + + public MJsonObject() throws MInvalidJsonValueException { + this("{}"); + } + + public MJsonObject(String x) throws MInvalidJsonValueException { + super(); + // + this.parseString(x); + } + + @Override + public MJsonObject clone() { + MJsonObject tmpMJsonObject = null; + // + try { + tmpMJsonObject = new MJsonObject(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonObject; + } + + /* Value handlers. */ + + public void setValue(String key, MJsonValue x) { + if (MText.isBlank(key)) { + throw new IllegalArgumentException("Invalid 'key': null or empty."); + } + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.getValues().put(key, x); + } + + protected LinkedHashMap getValues() { + return this.values; + } + + public MJsonValue getValue(String key) { + if (MText.isBlank(key)) { + throw new IllegalArgumentException("Invalid 'key': null or empty."); + } + // + if (!this.containsKey(key)) { + throw new IllegalArgumentException(String.format("Invalid 'key': %s: not available.", key)); + } + // + return this.getValues().get(key).clone(); + } + + public int getValueCount() { + return this.getValues().size(); + } + + public void removeValue(String key) { + if (MText.isBlank(key)) { + throw new IllegalArgumentException("Invalid 'key': null or empty."); + } + // + if (!this.containsKey(key)) { + throw new IllegalArgumentException(String.format("Invalid 'key': %s: not available.", key)); + } + // + this.getValues().remove(key); + } + + public void clearValues() { + this.getValues().clear(); + } + + public boolean containsKey(String key) { + if (MText.isBlank(key)) { + throw new IllegalArgumentException("Invalid 'key': null or empty."); + } + // + return this.getValues().containsKey(key); + } + + public LinkedHashSet getKeys() { + LinkedHashSet keys = new LinkedHashSet(); + for (String key: this.getValues().keySet()) { + keys.add(key); + } + // + return keys; + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if ((2 > x.length()) || ('{' != x.charAt(0))) { + return 0; + } + // + int position = x.indexOf("}", 1); + while (-1 < position) { + try { + MJsonObject testValue = new MJsonObject(); + testValue.parseString(x.substring(0, position + 1)); + // + return position + 1; + } + catch (MInvalidJsonValueException exception) { + position = x.indexOf("}", position + 1); + } + } + // + return 0; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + if ((2 > x.length()) || ('{' != x.charAt(0)) || ('}' != x.charAt(x.length() - 1))) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: must begin and end with curly brackets.", x)); + } + // + String y = x.substring(1, x.length() - 1); + int parsingPosition = 0; + MJsonObject.ParsingMode parsingMode = MJsonObject.ParsingMode.START; + String lastKey = ""; + while ((y.length() > parsingPosition) && (MJsonObject.ParsingMode.ERROR != parsingMode)) { + int c = y.codePointAt(parsingPosition); + switch (parsingMode) { + case START: + case POST_VALUE_SEPARATOR: // a name is expected + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if ((MJsonValue.isValueSeparator(c)) || (MJsonValue.isNameSeparator(c)) || (MJsonValue.isOpeningStructuralCharacter(c)) || (MJsonValue.isClosingStructuralCharacter(c))) { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + else { + int offset = MJsonString.getTokenLength(y.substring(parsingPosition)); + if (0 < offset) { + lastKey = (new MJsonString(y.substring(parsingPosition, parsingPosition + offset))).getValue(); + parsingPosition += offset; + parsingMode = MJsonObject.ParsingMode.POST_NAME; + } + else { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + } + break; + case POST_NAME: + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if (MJsonValue.isNameSeparator(c)) { + parsingPosition++; + parsingMode = MJsonObject.ParsingMode.POST_NAME_SEPARATOR; + } + else { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + break; + case POST_NAME_SEPARATOR: // a value is expected + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if ((MJsonValue.isValueSeparator(c)) || (MJsonValue.isNameSeparator(c)) || (MJsonValue.isClosingStructuralCharacter(c))) { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + else { + int offset = 0; + if (0 < (offset = MJsonNull.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonNull(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonBoolean.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonBoolean(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonNumber.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonNumber(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonString.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonString(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonArray.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonArray(y.substring(parsingPosition, parsingPosition + offset))); + } + else if (0 < (offset = MJsonObject.getTokenLength(y.substring(parsingPosition)))) { + this.setValue(lastKey, new MJsonObject(y.substring(parsingPosition, parsingPosition + offset))); + } + if (0 < offset) { + lastKey = ""; + parsingPosition += offset; + parsingMode = MJsonObject.ParsingMode.POST_VALUE; + } + else { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + } + break; + case POST_VALUE: + if (MJsonValue.isWhitespace(c)) { + parsingPosition++; + } + else if (MJsonValue.isValueSeparator(c)) { + parsingPosition++; + parsingMode = MJsonObject.ParsingMode.POST_VALUE_SEPARATOR; + } + else { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + break; + case ERROR: // malformed string + break; + } + } + // + if ((MJsonObject.ParsingMode.POST_NAME == parsingMode) || (MJsonObject.ParsingMode.POST_NAME_SEPARATOR == parsingMode) || (MJsonObject.ParsingMode.POST_VALUE_SEPARATOR == parsingMode)) { + parsingMode = MJsonObject.ParsingMode.ERROR; + } + if (MJsonObject.ParsingMode.ERROR == parsingMode) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json object.", x)); + } + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + StringBuilder s = new StringBuilder(""); + // + s.append("{"); + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + } + int k = 0; + for (String key: this.getValues().keySet()) { + if (0 < k) { + s.append(","); + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + } + else { + s.append(" "); + } + } + if (prettyPrintMode) { + for (int l = 0; (leftMargin + MConstants.JSON_PRETTY_PRINT_MODE_INDENTATION) > l; l++) { + s.append(" "); + } + } + s.append("\"" + MJsonString.getEscapedString(key, false) + "\""); + s.append(": "); + s.append(this.getValues().get(key).getJsonValue(prettyPrintMode, leftMargin + MConstants.JSON_PRETTY_PRINT_MODE_INDENTATION)); + k++; + } + if (prettyPrintMode) { + s.append(MConstants.JSON_PRETTY_PRINT_MODE_EOL); + for (int l = 0; leftMargin > l; l++) { + s.append(" "); + } + } + s.append("}"); + // + return s.toString(); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonString.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonString.java new file mode 100644 index 0000000..aac7f2a --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonString.java @@ -0,0 +1,314 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.text.MText; + +public class MJsonString extends MJsonValue { + + protected boolean extendedEscapeMode = false; + + protected String value = null; + + /* */ + + public MJsonString() throws MInvalidJsonValueException { + this("\"\""); + } + + public MJsonString(String x) throws MInvalidJsonValueException { + this(x, false); + } + + public MJsonString(String x, boolean extendedEscapeMode) throws MInvalidJsonValueException { + super(); + // + this.setExtendedEscapeMode(extendedEscapeMode); + this.parseString(x); + } + + @Override + public MJsonString clone() { + MJsonString tmpMJsonString = null; + // + try { + tmpMJsonString = new MJsonString(this.getJsonValue()); + } + catch (MInvalidJsonValueException exception) { // cannot happen + } + // + return tmpMJsonString; + } + + /* Extended escape mode. */ + + public void setExtendedEscapeMode(boolean extendedEscapeMode) { + this.extendedEscapeMode = extendedEscapeMode; + } + + public boolean getExtendedEscapeMode() { + return this.extendedEscapeMode; + } + + /* Value handlers. */ + + public void setValue(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + this.value = x; + } + + public String getValue() { + return this.value; + } + + /* Helpers. */ + + protected static String getEscapedString(String x, boolean extendedEscapeMode) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + StringBuilder y = new StringBuilder(); + for (int i = 0; x.length() > i; i++) { + int c = x.codePointAt(i); + if (extendedEscapeMode) { + if (0x00001F >= c) { // control Ascii character (0x000000-0x00001F) + y.append("\\u" + String.format("%4H", c).replace(" ", "0")); + } + else if (0x00007F >= c) { // non-control Ascii character (0x000020-0x00007F) + if ('"' == c) { + y.append("\\\""); + } + else if ('\\' == c) { + y.append("\\\\"); + } + else if ('/' == c) { + y.append("\\/"); + } + else { + y.appendCodePoint(c); + } + } + else if (0x00FFFF >= c) { // non-Ascii Bmp character (0x000080-0x00FFFF) + y.append("\\u" + String.format("%4H", c).replace(" ", "0")); + } + else { // non-Bmp character (0x010000-0x10FFFF) + int cc = c - 0x010000; + int ch = 0xD800 + (cc >> 10); + int cl = 0xDC00 + (cc & 0x03FF); + y.append("\\u" + String.format("%4H", ch).replace(" ", "0") + "\\u" + String.format("%4H", cl).replace(" ", "0")); + i++; + } + } + else { + if ('"' == c) { + y.append("\\\""); + } + else if ('\\' == c) { + y.append("\\\\"); + } + else if ('/' == c) { + y.append("\\/"); + } + else if ('\b' == c) { + y.append("\\b"); + } + else if ('\f' == c) { + y.append("\\f"); + } + else if ('\n' == c) { + y.append("\\n"); + } + else if ('\r' == c) { + y.append("\\r"); + } + else if ('\t' == c) { + y.append("\\t"); + } + else if (0x00001F >= c) { // non-special control Ascii character (0x000000-0x00001F) + y.append("\\u" + String.format("%4H", c).replace(" ", "0")); + } + else { + y.appendCodePoint(c); + if (0x010000 <= c) { + i++; + } + } + } + } + // + return y.toString(); + } + + protected static String getUnescapedString(String x) throws MInvalidJsonValueException { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + StringBuilder y = new StringBuilder(); + for (int i = 0; x.length() > i; i++) { + if (x.indexOf("\\\"", i) == i) { + y.append("\""); + i++; + } + else if (x.indexOf("\\\\", i) == i) { + y.append("\\"); + i++; + } + else if (x.indexOf("\\/", i) == i) { + y.append("/"); + i++; + } + else if (x.indexOf("\\b", i) == i) { + y.append("\b"); + i++; + } + else if (x.indexOf("\\f", i) == i) { + y.append("\f"); + i++; + } + else if (x.indexOf("\\n", i) == i) { + y.append("\n"); + i++; + } + else if (x.indexOf("\\r", i) == i) { + y.append("\r"); + i++; + } + else if (x.indexOf("\\t", i) == i) { + y.append("\t"); + i++; + } + else if (x.indexOf("\\u", i) == i) { + if ((i + 2 + 4) > x.length()) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + else { + int ch = -1; + try { + ch = Integer.parseInt(x.substring(i + 2, i + 2 + 4), 16); + i += 5; + if ((0xD800 <= ch) && (0xDBFF >= ch)) { + i++; + if (x.indexOf("\\u", i) != i) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + else if ((i + 2 + 4) > x.length()) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + else { + int cl = -1; + try { + cl = Integer.parseInt(x.substring(i + 2, i + 2 + 4), 16); + i += 5; + if ((0xDC00 >= cl) || (0xDFFF <= cl)) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + int c = ((ch - 0xD800) << 10) + (cl - 0xDC00) + 0x010000; + y.appendCodePoint(c); + } + catch (NumberFormatException exception) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); // no need to propagate exception + } + } + } + else { + y.appendCodePoint(ch); + } + } + catch (NumberFormatException exception) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); // no need to propagate exception + } + } + } + else { + int c = x.codePointAt(i); + if (0x010000 <= c) { + i++; + } + y.appendCodePoint(c); + } + } + // + return y.toString(); + } + + /* Parsers. */ + + protected static int getTokenLength(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + if ((2 > x.length()) || ('"' != x.charAt(0))) { + return 0; + } + if ('"' == x.charAt(1)) { + return 1 + 1; + } + // + String y = x.substring(1); + int quotationMarkPosition = 0; + while (-1 < (quotationMarkPosition = y.indexOf("\"", quotationMarkPosition + 1))) { + int c = 0; + StringBuilder s = new StringBuilder(""); + while ((c <= quotationMarkPosition) && (y.substring(quotationMarkPosition - c, quotationMarkPosition).equals(s.toString()))) { + c++; + s.append("\\"); + } + c--; + if (0 == (c % 2)) { + break; + } + } + // + if (-1 == quotationMarkPosition) { + return 0; + } + // + return (quotationMarkPosition + 1) + 1; + } + + public void parseString(String x) throws MInvalidJsonValueException { + if (MText.isBlank(x)) { + throw new IllegalArgumentException("Invalid 'x': null or empty."); + } + // + x = x.replaceAll("\\R", ""); + // + if ((2 > x.length()) || ('"' != x.charAt(0)) || ('"' != x.charAt(x.length() - 1))) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + // + String y = x.substring(1, x.length() - 1); + if (-1 < y.replaceAll("\\\\\"", "__").indexOf("\"")) { + throw new MInvalidJsonValueException(String.format("Invalid 'x': %s: not a Json string.", x)); + } + // + this.setValue(MJsonString.getUnescapedString(y)); + } + + /* Formatter. */ + + @Override + public String getJsonValue() { + return this.getJsonValue(false); + } + + @Override + public String getJsonValue(boolean prettyPrintMode) { + return this.getJsonValue(prettyPrintMode, 0); + } + + @Override + protected String getJsonValue(boolean prettyPrintMode, int leftMargin) { + return "\"" + MJsonString.getEscapedString(this.getValue(), this.getExtendedEscapeMode()) + "\""; + } +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/json/MJsonValue.java b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonValue.java new file mode 100644 index 0000000..9518e94 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/json/MJsonValue.java @@ -0,0 +1,48 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.json; + +import com.marcozanon.macaco.MObject; + +public abstract class MJsonValue extends MObject { + + /* */ + + @Override + public abstract MJsonValue clone(); + + /* Helpers. */ + + protected static boolean isWhitespace(int c) { + return ((' ' == c) || ('\t' == c) || ('\n' == c) || ('\r' == c)); + } + + protected static boolean isValueSeparator(int c) { + return (',' == c); + } + + protected static boolean isNameSeparator(int c) { + return (':' == c); + } + + protected static boolean isOpeningStructuralCharacter(int c) { + return (('[' == c) || ('{' == c)); + } + + protected static boolean isClosingStructuralCharacter(int c) { + return ((']' == c) || ('}' == c)); + } + + /* Formatter. */ + + public abstract String getJsonValue(); + + public abstract String getJsonValue(boolean prettyPrintMode); + + protected abstract String getJsonValue(boolean prettyPrintMode, int leftMargin); + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicenseManager.java b/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicenseManager.java new file mode 100644 index 0000000..f99df29 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicenseManager.java @@ -0,0 +1,167 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.licensing; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.json.MInvalidJsonValueException; +import com.marcozanon.macaco.json.MJsonObject; +import com.marcozanon.macaco.json.MJsonString; +import com.marcozanon.macaco.text.MText; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class MLicenseManager { + + /* License generation. */ + + public static void generateLicense(Path privateKeyFile, Path licenseSkeletonFile, Path licenseFile) throws MLicensingException { + if (null == privateKeyFile) { + throw new IllegalArgumentException("Invalid 'privateKeyFile': null."); + } + if (null == licenseSkeletonFile) { + throw new IllegalArgumentException("Invalid 'licenseSkeletonFile': null."); + } + if (null == licenseFile) { + throw new IllegalArgumentException("Invalid 'licenseFile': null."); + } + // + try { + String privateKeyPemString = new String(Files.readAllBytes(privateKeyFile), MConstants.DEFAULT_CHARSET); + String licenseSkeletonJsonString = new String(Files.readAllBytes(licenseSkeletonFile), MConstants.DEFAULT_CHARSET); + // + MJsonObject license = MLicenseManager.generateLicense(privateKeyPemString, licenseSkeletonJsonString); + // + Files.write(licenseFile, license.getJsonValue(true).getBytes(MConstants.DEFAULT_CHARSET), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + catch (IOException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + } + + public static MJsonObject generateLicense(String privateKeyPemString, String licenseSkeletonJsonString) throws MLicensingException { + if (MText.isBlank(privateKeyPemString)) { + throw new IllegalArgumentException("Invalid 'privateKeyPemString': null or empty."); + } + if (MText.isBlank(licenseSkeletonJsonString)) { + throw new IllegalArgumentException("Invalid 'licenseSkeletonJsonString': null or empty."); + } + // + try { + byte[] privateKeyContent = Base64.getDecoder().decode(privateKeyPemString.replace("-----BEGIN PRIVATE KEY-----", "").replaceAll("\\R", "").replace("-----END PRIVATE KEY-----", "")); + RSAPrivateKey privateKey = (RSAPrivateKey)KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyContent)); + // + MJsonObject licenseSkeleton = new MJsonObject(licenseSkeletonJsonString); + // + MJsonObject licenseData = (MJsonObject)licenseSkeleton.getValue("licenseData"); + byte[] licenseDataHashContent = MessageDigest.getInstance("SHA-256").digest(licenseData.getJsonValue().getBytes(MConstants.DEFAULT_CHARSET)); + // + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign​(privateKey); + signature.update(licenseDataHashContent); + byte[] licenseDataHashSignatureContent = signature.sign(); + // + licenseSkeleton.setValue("licenseDataSignature", new MJsonString("\"" + Base64.getEncoder().encodeToString(licenseDataHashSignatureContent) + "\"")); + // + return licenseSkeleton; + } + catch (InvalidKeyException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + catch (InvalidKeySpecException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + catch (MInvalidJsonValueException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + catch (NoSuchAlgorithmException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + catch (SignatureException exception) { + throw new MLicensingException("Could not generate license.", exception); + } + } + + /* License verification. */ + + public static MJsonObject verifyLicense(Path publicKeyFile, Path licenseFile) throws MLicensingException { + if (null == publicKeyFile) { + throw new IllegalArgumentException("Invalid 'publicKeyFile': null."); + } + if (null == licenseFile) { + throw new IllegalArgumentException("Invalid 'licenseFile': null."); + } + // + try { + String publicKeyPemString = new String(Files.readAllBytes(publicKeyFile), MConstants.DEFAULT_CHARSET); + String licenseJsonString = new String(Files.readAllBytes(licenseFile), MConstants.DEFAULT_CHARSET); + // + return MLicenseManager.verifyLicense(publicKeyPemString, licenseJsonString); + } + catch (IOException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + } + + public static MJsonObject verifyLicense(String publicKeyPemString, String licenseJsonString) throws MLicensingException { + if (MText.isBlank(publicKeyPemString)) { + throw new IllegalArgumentException("Invalid 'publicKeyPemString': null or empty."); + } + if (MText.isBlank(licenseJsonString)) { + throw new IllegalArgumentException("Invalid 'licenseJsonString': null or empty."); + } + // + try { + byte[] publicKeyContent = Base64.getDecoder().decode(publicKeyPemString.replace("-----BEGIN PUBLIC KEY-----", "").replaceAll("\\R", "").replace("-----END PUBLIC KEY-----", "")); + RSAPublicKey publicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyContent)); + // + MJsonObject license = new MJsonObject(licenseJsonString); + // + MJsonObject licenseData = (MJsonObject)license.getValue("licenseData"); + byte[] licenseDataHashContent = MessageDigest.getInstance("SHA-256").digest(licenseData.getJsonValue().getBytes(MConstants.DEFAULT_CHARSET)); + // + MJsonString licenseDataSignature = (MJsonString)license.getValue("licenseDataSignature"); + byte[] licenseDataSignatureContent = Base64.getDecoder().decode(licenseDataSignature.getValue()); + // + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initVerify​(publicKey); + signature.update(licenseDataHashContent); + signature.verify(licenseDataSignatureContent); + // + return license; + } + catch (InvalidKeyException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + catch (InvalidKeySpecException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + catch (MInvalidJsonValueException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + catch (NoSuchAlgorithmException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + catch (SignatureException exception) { + throw new MLicensingException("Could not verify license.", exception); + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicensingException.java b/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicensingException.java new file mode 100644 index 0000000..c5ac150 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/licensing/MLicensingException.java @@ -0,0 +1,32 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.licensing; + +import com.marcozanon.macaco.MException; + +@SuppressWarnings("serial") +public class MLicensingException extends MException { + + /* */ + + public MLicensingException() { + super(); + } + + public MLicensingException(String message) { + super(message); + } + + public MLicensingException(Throwable error) { + super(error); + } + + public MLicensingException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogDatabaseTable.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogDatabaseTable.java new file mode 100644 index 0000000..c6ce3ef --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogDatabaseTable.java @@ -0,0 +1,118 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.database.MDatabaseConnection; +import com.marcozanon.macaco.database.MDatabaseConnectionFailureException; +import com.marcozanon.macaco.database.MDatabaseConnectionPool; +import com.marcozanon.macaco.database.MSqlStatementException; +import com.marcozanon.macaco.database.MSqlTable; +import com.marcozanon.macaco.text.MText; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; + +public class MLogDatabaseTable extends MLogTarget { + + protected MDatabaseConnectionPool databaseConnectionPool = null; + + protected String logDatabaseTable = null; + protected String logDatabaseTablePrimaryKey = null; + protected String logDatabaseField = null; + + protected MDatabaseConnection databaseConnection = null; + + /* */ + + public MLogDatabaseTable(MDatabaseConnectionPool databaseConnectionPool, String logDatabaseTable, String logDatabaseTablePrimaryKey, String logDatabaseField) throws MLoggingException { + super(); + // + if (null == databaseConnectionPool) { + throw new IllegalArgumentException("Invalid 'databaseConnectionPool': null."); + } + if (MText.isEmpty(logDatabaseTable)) { + throw new IllegalArgumentException("Invalid 'logDatabaseTable': null or empty."); + } + if (MText.isEmpty(logDatabaseTablePrimaryKey)) { + throw new IllegalArgumentException("Invalid 'logDatabaseTablePrimaryKey': null or empty."); + } + if (MText.isEmpty(logDatabaseField)) { + throw new IllegalArgumentException("Invalid 'logDatabaseField': null or empty."); + } + // + this.databaseConnectionPool = databaseConnectionPool; + this.logDatabaseTable = logDatabaseTable; + this.logDatabaseTablePrimaryKey = logDatabaseTablePrimaryKey; + this.logDatabaseField = logDatabaseField; + } + + @Override + public void close() throws MLoggingException { + } + + /* Database connection pool. */ + + protected MDatabaseConnectionPool getDatabaseConnectionPool() { + return this.databaseConnectionPool; + } + + /* Database logging parameters. */ + + protected String getLogDatabaseTable() { + return this.logDatabaseTable; + } + + protected String getLogDatabaseTablePrimaryKey() { + return this.logDatabaseTablePrimaryKey; + } + + protected String getLogDatabaseField() { + return this.logDatabaseField; + } + + /* Output. */ + + @Override + public void appendMessage(String message) throws MLoggingException { + this.appendMessage(message, 0); + } + + @Override + public void appendMessage(String message, int indentation) throws MLoggingException { + if (null == message) { + throw new IllegalArgumentException("Invalid 'message': null."); + } + if (0 > indentation) { + throw new IllegalArgumentException(String.format("Invalid 'indentation': %s: cannot be negative.", indentation)); + } + // + try { + String timestamp = (new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")).format(new Date()); + // + StringBuilder separator = new StringBuilder(" "); + for (int i = 0; i < indentation; i++) { + separator.append(" "); + } + // + MDatabaseConnection databaseConnection = this.getDatabaseConnectionPool().popDatabaseConnection(); + // + LinkedHashMap p = new LinkedHashMap(); + p.put(this.getLogDatabaseField(), timestamp + separator + message); + // + (new MSqlTable(databaseConnection, this.getLogDatabaseTable(), this.getLogDatabaseTablePrimaryKey())).setRecord(p, null, false); + // + this.getDatabaseConnectionPool().pushDatabaseConnection(databaseConnection); + } + catch (MDatabaseConnectionFailureException exception) { + throw new MLoggingException("Could not write to database table.", exception); + } + catch (MSqlStatementException exception) { + throw new MLoggingException("Could not write to database table.", exception); + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilter.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilter.java new file mode 100644 index 0000000..b01998d --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilter.java @@ -0,0 +1,180 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.MObject; +import java.time.LocalDateTime; +import java.util.LinkedList; + +public class MLogFilter extends MObject { + + public static enum Threshold { + STANDARD, + DEBUG + }; + + protected MLogFilter.Threshold threshold = null; + + protected LinkedList logTargets = new LinkedList(); + + protected boolean pausedState = false; + protected LinkedList logMessageQueue = new LinkedList(); + + protected LocalDateTime lastLogMessageDatetime = null; + protected MLogFilterMarkMessageAppender logFilterMarkMessageAppender = null; + + /* */ + + public MLogFilter(MLogFilter.Threshold threshold) { + super(); + // + this.setThreshold(threshold); + // + this.setLogFilterMarkMessageAppender(); + } + + public void close() throws MLoggingException { + this.getLogFilterMarkMessageAppender().setRunMode(MLogFilterMarkMessageAppender.RunMode.STOPPED); + // + for (MLogTarget t: this.getLogTargets()) { + t.close(); + } + } + + @Override + protected void finalize() { + try { + this.close(); + } + catch (Exception exception) { + } + } + + /* Threshold. */ + + public void setThreshold(MLogFilter.Threshold threshold) { + if (null == threshold) { + throw new IllegalArgumentException("Invalid 'threshold': null."); + } + // + this.threshold = threshold; + } + + public MLogFilter.Threshold getThreshold() { + return this.threshold; + } + + /* Log targets. */ + + public void addLogTarget(MLogTarget logTarget) { + if (null == logTarget) { + throw new IllegalArgumentException("Invalid 'logTarget': null."); + } + // + synchronized (this.logTargets) { + this.logTargets.add(logTarget); + } + } + + protected LinkedList getLogTargets() { + return this.logTargets; + } + + /* Mark messages. */ + + protected void setLogFilterMarkMessageAppender() { + this.updateLastLogMessageDatetime(); + // + this.logFilterMarkMessageAppender = new MLogFilterMarkMessageAppender(this); + // + (new Thread(this.getLogFilterMarkMessageAppender(), MConstants.LOG_FILTER_MARK_MESSAGE_APPENDER_THREAD_ID)).start(); + } + + protected MLogFilterMarkMessageAppender getLogFilterMarkMessageAppender() { + return this.logFilterMarkMessageAppender; + } + + protected synchronized void updateLastLogMessageDatetime() { + this.lastLogMessageDatetime = LocalDateTime.now(); + } + + public LocalDateTime getLastLogMessageDatetime() { + return this.lastLogMessageDatetime; + } + + /* Output. */ + + public synchronized void setPausedState(boolean pausedState) throws MLoggingException { + this.pausedState = pausedState; + // + if (!this.getPausedState()) { + this.flushMessages(); + } + } + + public boolean getPausedState() { + return this.pausedState; + } + + public synchronized boolean appendMessage(MLogFilter.Threshold level, String message) throws MLoggingException { + return this.appendMessage(level, message, 0); + } + + public synchronized boolean appendSafeMessage(MLogFilter.Threshold level, String message) { + try { + return this.appendMessage(level, message); + } + catch (MLoggingException exception) { + } + // + return false; + } + + public synchronized boolean appendMessage(MLogFilter.Threshold level, String message, int indentation) throws MLoggingException { + if (null == level) { + throw new IllegalArgumentException("Invalid 'level': null."); + } + // + if (level.ordinal() > this.getThreshold().ordinal()) { + return false; + } + // + this.logMessageQueue.add(new MLogMessage(message, indentation)); + // + if (!this.getPausedState()) { + this.flushMessages(); + // + this.updateLastLogMessageDatetime(); + } + // + return true; + } + + public synchronized boolean appendSafeMessage(MLogFilter.Threshold level, String message, int indentation) { + try { + return this.appendMessage(level, message, indentation); + } + catch (MLoggingException exception) { + } + // + return false; + } + + protected void flushMessages() throws MLoggingException { + while (0 < this.logMessageQueue.size()) { + MLogMessage logMessage = this.logMessageQueue.remove(); + String message = logMessage.getMessage(); + int indentation = logMessage.getIndentation(); + // + for (MLogTarget logTarget: this.getLogTargets()) { + logTarget.appendMessage(message, indentation); + } + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilterMarkMessageAppender.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilterMarkMessageAppender.java new file mode 100644 index 0000000..4be2315 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogFilterMarkMessageAppender.java @@ -0,0 +1,78 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MConstants; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +public class MLogFilterMarkMessageAppender implements Runnable { + + public static enum RunMode { + INITIALIZING, + RUNNING, + STOPPED; + } + + protected MLogFilter logFilter = null; + + protected MLogFilterMarkMessageAppender.RunMode runMode = null; + + /* */ + + public MLogFilterMarkMessageAppender(MLogFilter logFilter) { + this.setRunMode(MLogFilterMarkMessageAppender.RunMode.INITIALIZING); + // + this.setLogFilter(logFilter); + } + + /* Log filter. */ + + protected void setLogFilter(MLogFilter logFilter) { + if (null == logFilter) { + throw new IllegalArgumentException("Invalid 'logFilter': null."); + } + // + this.logFilter = logFilter; + } + + protected MLogFilter getLogFilter() { + return this.logFilter; + } + + /* Threading. */ + + public synchronized void setRunMode(MLogFilterMarkMessageAppender.RunMode runMode) { + if (null == runMode) { + throw new IllegalArgumentException("Invalid 'runMode': null."); + } + // + this.runMode = runMode; + } + + public synchronized MLogFilterMarkMessageAppender.RunMode getRunMode() { + return this.runMode; + } + + @Override + public void run() { + this.setRunMode(MLogFilterMarkMessageAppender.RunMode.RUNNING); + // + while (MLogFilterMarkMessageAppender.RunMode.RUNNING == this.getRunMode()) { + if (MConstants.LOG_FILTER_MARK_INTERVAL <= ChronoUnit.SECONDS.between(this.getLogFilter().getLastLogMessageDatetime(), LocalDateTime.now())) { + this.getLogFilter().appendSafeMessage(MLogFilter.Threshold.STANDARD, MConstants.LOG_FILTER_MARK_MESSAGE); + } + // + try { + Thread.sleep(MConstants.LOG_FILTER_MARK_MESSAGE_APPENDER_SLEEP_INTERVAL); + } + catch (InterruptedException exception) { + } + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogListener.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogListener.java new file mode 100644 index 0000000..3ea5531 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogListener.java @@ -0,0 +1,15 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +public interface MLogListener { + + /* Logging. */ + + public void onMessageLogging(String message); + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogMessage.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogMessage.java new file mode 100644 index 0000000..348fd43 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogMessage.java @@ -0,0 +1,40 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MObject; + +public class MLogMessage extends MObject { + + protected String message = null; + protected int indentation = 0; + + /* */ + + public MLogMessage(String message, int indentation) { + if (null == message) { + throw new IllegalArgumentException("Invalid 'message': null."); + } + if (0 > indentation) { + throw new IllegalArgumentException(String.format("Invalid 'indentation': %s: cannot be negative.", indentation)); + } + // + this.message = message; + this.indentation = indentation; + } + + /* Values. */ + + public String getMessage() { + return this.message; + } + + public int getIndentation() { + return this.indentation; + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogPlainTextFile.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogPlainTextFile.java new file mode 100644 index 0000000..98dc2aa --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogPlainTextFile.java @@ -0,0 +1,100 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MConstants; +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class MLogPlainTextFile extends MLogTarget { + + protected Path file = null; + + protected BufferedWriter buffer = null; + + /* */ + + public MLogPlainTextFile(Path file) throws MLoggingException { + super(); + // + if (null == file) { + throw new IllegalArgumentException("Invalid 'file': null."); + } + // + this.file = file; + try { + this.buffer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.getFile().toFile(), true), MConstants.DEFAULT_CHARSET)); + } + catch (FileNotFoundException exception) { + throw new MLoggingException("Could not open file.", exception); + } + } + + @Override + public void close() throws MLoggingException { + try { + this.getBuffer().close(); + } + catch (IOException exception) { + throw new MLoggingException("Could not close file.", exception); + } + } + + /* File. */ + + protected Path getFile() { + return this.file; + } + + /* Buffer. */ + + protected BufferedWriter getBuffer() { + return this.buffer; + } + + /* Output. */ + + @Override + public void appendMessage(String message) throws MLoggingException { + this.appendMessage(message, 0); + } + + @Override + public void appendMessage(String message, int indentation) throws MLoggingException { + if (null == message) { + throw new IllegalArgumentException("Invalid 'message': null."); + } + if (0 > indentation) { + throw new IllegalArgumentException(String.format("Invalid 'indentation': %s: cannot be negative.", indentation)); + } + // + try { + String timestamp = (new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")).format(new Date()); + // + StringBuilder separator = new StringBuilder(" "); + for (int i = 0; i < indentation; i++) { + separator.append(" "); + } + // + synchronized (this.getBuffer()) { + this.getBuffer().write(timestamp + separator + message); + this.getBuffer().newLine(); + this.getBuffer().flush(); + } + } + catch (IOException exception) { + throw new MLoggingException("Could not write to file.", exception); + } + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLogTarget.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogTarget.java new file mode 100644 index 0000000..a1332d2 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLogTarget.java @@ -0,0 +1,32 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MObject; + +public abstract class MLogTarget extends MObject { + + /* */ + + public abstract void close() throws MLoggingException; + + @Override + protected void finalize() { + try { + this.close(); + } + catch (Exception exception) { + } + } + + /* Output. */ + + public abstract void appendMessage(String message) throws MLoggingException; + + public abstract void appendMessage(String message, int indentation) throws MLoggingException; + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/logging/MLoggingException.java b/10.x/src/main/java/com/marcozanon/macaco/logging/MLoggingException.java new file mode 100644 index 0000000..67c1415 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/logging/MLoggingException.java @@ -0,0 +1,32 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.logging; + +import com.marcozanon.macaco.MException; + +@SuppressWarnings("serial") +public class MLoggingException extends MException { + + /* */ + + public MLoggingException() { + super(); + } + + public MLoggingException(String message) { + super(message); + } + + public MLoggingException(Throwable error) { + super(error); + } + + public MLoggingException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MText.java b/10.x/src/main/java/com/marcozanon/macaco/text/MText.java new file mode 100644 index 0000000..6c9268c --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MText.java @@ -0,0 +1,488 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon . + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.MObject; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.StringJoiner; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +public class MText extends MObject { + + /* Checks. */ + + public static boolean isEmpty(String x) { + if ((null != x) && ("".equals(x))) { + return true; + } + // + return false; + } + + public static boolean isBlank(String x) { + if ((null == x) || ("".equals(x))) { + return true; + } + // + return false; + } + + /* Utility methods. */ + + public static String repeat(String x, int times) { + if (1 > times) { + throw new IllegalArgumentException(String.format("Invalid 'times': %s.", times)); + } + // + StringBuilder y = new StringBuilder(""); + // + for (int i = 0; i < times; i++) { + y = y.append(x); + } + // + return y.toString(); + } + + public static String getRandomToken(int length) { + if (0 > length) { + throw new IllegalArgumentException(String.format("Invalid 'length': %s.", length)); + } + // + StringBuilder y = new StringBuilder(""); + // + for (int i = 0; i < length; i++) { + char c = (char)(65 + (25 * Math.random())); + y = y.append(c); + } + // + return y.toString(); + } + + public static String removeLine(String x, int lineNumber) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + String[] xLines = x.split(System.getProperty("line.separator")); + // + if ((0 > lineNumber) || (xLines.length - 1 < lineNumber)) { + throw new IllegalArgumentException(String.format("Invalid 'lineNumber': %s.", lineNumber)); + } + // + StringJoiner stringJoiner = new StringJoiner(System.getProperty("line.separator")); + for (int l = 0; xLines.length > l; l++) { + if (l == lineNumber) { + continue; + } + // + stringJoiner.add(xLines[l]); + } + // + return stringJoiner.toString(); + } + + public static String removeFirstLine(String x) { + return MText.removeLine(x, 0); + } + + public static String removeLastLine(String x) { + if (null == x) { + throw new IllegalArgumentException("Invalid 'x': null."); + } + // + String[] xLines = x.split(System.getProperty("line.separator")); + // + return MText.removeLine(x, xLines.length - 1); + } + + /* Javascript escape. */ + + public static String getJavascriptEscapedString(String x) { + if (null == x) { + return null; + } + // + String text = x; + // + text = text.replace("\\", "\\\\"); + text = text.replace("'", "\\\'"); + // + return text; + } + + /* XHTML escape and safety. */ + + public static String getXhtmlEscapedString(String x) { // similar to PHP's htmlspecialchars() + if (null == x) { + return null; + } + // + String text = x; + // + text = text.replace("&", "&"); + text = text.replace("\"", """); + text = text.replace("'", "'"); + text = text.replace("<", "<"); + text = text.replace(">", ">"); + // + text = text.replace("\n", "
"); + // + return text; + } + + public static String getXhtmlNumericEntitiesString(String x) { // for compatibility with innerHTML + if (null == x) { + return null; + } + // + String text = x; + // + text = text.replace(" ", " "); + text = text.replace("¡", "¡"); + text = text.replace("¢", "¢"); + text = text.replace("£", "£"); + text = text.replace("¤", "¤"); + text = text.replace("¥", "¥"); + text = text.replace("¦", "¦"); + text = text.replace("§", "§"); + text = text.replace("¨", "¨"); + text = text.replace("©", "©"); + text = text.replace("ª", "ª"); + text = text.replace("«", "«"); + text = text.replace("¬", "¬"); + text = text.replace("­", "­"); + text = text.replace("®", "®"); + text = text.replace("¯", "¯"); + text = text.replace("°", "°"); + text = text.replace("±", "±"); + text = text.replace("²", "²"); + text = text.replace("³", "³"); + text = text.replace("´", "´"); + text = text.replace("µ", "µ"); + text = text.replace("¶", "¶"); + text = text.replace("·", "·"); + text = text.replace("¸", "¸"); + text = text.replace("¹", "¹"); + text = text.replace("º", "º"); + text = text.replace("»", "»"); + text = text.replace("¼", "¼"); + text = text.replace("½", "½"); + text = text.replace("¾", "¾"); + text = text.replace("¿", "¿"); + text = text.replace("À", "À"); + text = text.replace("Á", "Á"); + text = text.replace("Â", "Â"); + text = text.replace("Ã", "Ã"); + text = text.replace("Ä", "Ä"); + text = text.replace("Å", "Å"); + text = text.replace("Æ", "Æ"); + text = text.replace("Ç", "Ç"); + text = text.replace("È", "È"); + text = text.replace("É", "É"); + text = text.replace("Ê", "Ê"); + text = text.replace("Ë", "Ë"); + text = text.replace("Ì", "Ì"); + text = text.replace("Í", "Í"); + text = text.replace("Î", "Î"); + text = text.replace("Ï", "Ï"); + text = text.replace("Ð", "Ð"); + text = text.replace("Ñ", "Ñ"); + text = text.replace("Ò", "Ò"); + text = text.replace("Ó", "Ó"); + text = text.replace("Ô", "Ô"); + text = text.replace("Õ", "Õ"); + text = text.replace("Ö", "Ö"); + text = text.replace("×", "×"); + text = text.replace("Ø", "Ø"); + text = text.replace("Ù", "Ù"); + text = text.replace("Ú", "Ú"); + text = text.replace("Û", "Û"); + text = text.replace("Ü", "Ü"); + text = text.replace("Ý", "Ý"); + text = text.replace("Þ", "Þ"); + text = text.replace("ß", "ß"); + text = text.replace("à", "à"); + text = text.replace("á", "á"); + text = text.replace("â", "â"); + text = text.replace("ã", "ã"); + text = text.replace("ä", "ä"); + text = text.replace("å", "å"); + text = text.replace("æ", "æ"); + text = text.replace("ç", "ç"); + text = text.replace("è", "è"); + text = text.replace("é", "é"); + text = text.replace("ê", "ê"); + text = text.replace("ë", "ë"); + text = text.replace("ì", "ì"); + text = text.replace("í", "í"); + text = text.replace("î", "î"); + text = text.replace("ï", "ï"); + text = text.replace("ð", "ð"); + text = text.replace("ñ", "ñ"); + text = text.replace("ò", "ò"); + text = text.replace("ó", "ó"); + text = text.replace("ô", "ô"); + text = text.replace("õ", "õ"); + text = text.replace("ö", "ö"); + text = text.replace("÷", "÷"); + text = text.replace("ø", "ø"); + text = text.replace("ù", "ù"); + text = text.replace("ú", "ú"); + text = text.replace("û", "û"); + text = text.replace("ü", "ü"); + text = text.replace("ý", "ý"); + text = text.replace("þ", "þ"); + text = text.replace("ÿ", "ÿ"); + text = text.replace("Œ", "Œ"); + text = text.replace("œ", "œ"); + text = text.replace("Š", "Š"); + text = text.replace("š", "š"); + text = text.replace("Ÿ", "Ÿ"); + text = text.replace("ƒ", "ƒ"); + text = text.replace("ˆ", "ˆ"); + text = text.replace("˜", "˜"); + text = text.replace("Α", "Α"); + text = text.replace("Β", "Β"); + text = text.replace("Γ", "Γ"); + text = text.replace("Δ", "Δ"); + text = text.replace("Ε", "Ε"); + text = text.replace("Ζ", "Ζ"); + text = text.replace("Η", "Η"); + text = text.replace("Θ", "Θ"); + text = text.replace("Ι", "Ι"); + text = text.replace("Κ", "Κ"); + text = text.replace("Λ", "Λ"); + text = text.replace("Μ", "Μ"); + text = text.replace("Ν", "Ν"); + text = text.replace("Ξ", "Ξ"); + text = text.replace("Ο", "Ο"); + text = text.replace("Π", "Π"); + text = text.replace("Ρ", "Ρ"); + text = text.replace("Σ", "Σ"); + text = text.replace("Τ", "Τ"); + text = text.replace("Υ", "Υ"); + text = text.replace("Φ", "Φ"); + text = text.replace("Χ", "Χ"); + text = text.replace("Ψ", "Ψ"); + text = text.replace("Ω", "Ω"); + text = text.replace("α", "α"); + text = text.replace("β", "β"); + text = text.replace("γ", "γ"); + text = text.replace("δ", "δ"); + text = text.replace("ε", "ε"); + text = text.replace("ζ", "ζ"); + text = text.replace("η", "η"); + text = text.replace("θ", "θ"); + text = text.replace("ι", "ι"); + text = text.replace("κ", "κ"); + text = text.replace("λ", "λ"); + text = text.replace("μ", "μ"); + text = text.replace("ν", "ν"); + text = text.replace("ξ", "ξ"); + text = text.replace("ο", "ο"); + text = text.replace("π", "π"); + text = text.replace("ρ", "ρ"); + text = text.replace("ς", "ς"); + text = text.replace("σ", "σ"); + text = text.replace("τ", "τ"); + text = text.replace("υ", "υ"); + text = text.replace("φ", "φ"); + text = text.replace("χ", "χ"); + text = text.replace("ψ", "ψ"); + text = text.replace("ω", "ω"); + text = text.replace("ϑ", "ϑ"); + text = text.replace("ϒ", "ϒ"); + text = text.replace("ϖ", "ϖ"); + text = text.replace(" ", " "); + text = text.replace(" ", " "); + text = text.replace(" ", " "); + text = text.replace("‌", "‌"); + text = text.replace("‍", "‍"); + text = text.replace("‎", "‎"); + text = text.replace("‏", "‏"); + text = text.replace("–", "–"); + text = text.replace("—", "—"); + text = text.replace("‘", "‘"); + text = text.replace("’", "’"); + text = text.replace("‚", "‚"); + text = text.replace("“", "“"); + text = text.replace("”", "”"); + text = text.replace("„", "„"); + text = text.replace("†", "†"); + text = text.replace("‡", "‡"); + text = text.replace("•", "•"); + text = text.replace("…", "…"); + text = text.replace("‰", "‰"); + text = text.replace("′", "′"); + text = text.replace("″", "″"); + text = text.replace("‹", "‹"); + text = text.replace("›", "›"); + text = text.replace("‾", "‾"); + text = text.replace("⁄", "⁄"); + text = text.replace("€", "€"); + text = text.replace("ℑ", "ℑ"); + text = text.replace("℘", "℘"); + text = text.replace("ℜ", "ℜ"); + text = text.replace("™", "™"); + text = text.replace("ℵ", "ℵ"); + text = text.replace("←", "←"); + text = text.replace("↑", "↑"); + text = text.replace("→", "→"); + text = text.replace("↓", "↓"); + text = text.replace("↔", "↔"); + text = text.replace("↵", "↵"); + text = text.replace("⇐", "⇐"); + text = text.replace("⇑", "⇑"); + text = text.replace("⇒", "⇒"); + text = text.replace("⇓", "⇓"); + text = text.replace("⇔", "⇔"); + text = text.replace("∀", "∀"); + text = text.replace("∂", "∂"); + text = text.replace("∃", "∃"); + text = text.replace("∅", "∅"); + text = text.replace("∇", "∇"); + text = text.replace("∈", "∈"); + text = text.replace("∉", "∉"); + text = text.replace("∋", "∋"); + text = text.replace("∏", "∏"); + text = text.replace("∑", "∑"); + text = text.replace("−", "−"); + text = text.replace("∗", "∗"); + text = text.replace("√", "√"); + text = text.replace("∝", "∝"); + text = text.replace("∞", "∞"); + text = text.replace("∠", "∠"); + text = text.replace("∧", "∧"); + text = text.replace("∨", "∨"); + text = text.replace("∩", "∩"); + text = text.replace("∪", "∪"); + text = text.replace("∫", "∫"); + text = text.replace("∴", "∴"); + text = text.replace("∼", "∼"); + text = text.replace("≅", "≅"); + text = text.replace("≈", "≈"); + text = text.replace("≠", "≠"); + text = text.replace("≡", "≡"); + text = text.replace("≤", "≤"); + text = text.replace("≥", "≥"); + text = text.replace("⊂", "⊂"); + text = text.replace("⊃", "⊃"); + text = text.replace("⊄", "⊄"); + text = text.replace("⊆", "⊆"); + text = text.replace("⊇", "⊇"); + text = text.replace("⊕", "⊕"); + text = text.replace("⊗", "⊗"); + text = text.replace("⊥", "⊥"); + text = text.replace("⋅", "⋅"); + text = text.replace("⌈", "⌈"); + text = text.replace("⌉", "⌉"); + text = text.replace("⌊", "⌊"); + text = text.replace("⌋", "⌋"); + text = text.replace("⟨", "〈"); + text = text.replace("⟩", "〉"); + text = text.replace("◊", "◊"); + text = text.replace("♠", "♠"); + text = text.replace("♣", "♣"); + text = text.replace("♥", "♥"); + text = text.replace("♦", "♦"); + // + return text; + } + + public static String getXhtmlSafeString(String x) throws MXhtmlUnsafeStringException { + if (null == x) { + return null; + } + // + String text = x; + // + StringBuilder fakeXhtmlPageContent = new StringBuilder(""); + fakeXhtmlPageContent.append(String.format("", MConstants.DEFAULT_CHARSET)); + fakeXhtmlPageContent.append(""); + fakeXhtmlPageContent.append(""); + fakeXhtmlPageContent.append("</head>"); + fakeXhtmlPageContent.append(String.format("<body>%s</body>", text)); + fakeXhtmlPageContent.append("</html>"); + // Validate XHTML (with no dangerous tags and event attributes). + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setValidating(true); + factory.setNamespaceAware(true); + SAXParser parser = factory.newSAXParser(); + XMLReader reader = parser.getXMLReader(); + reader.setEntityResolver(new EntityResolver() { + + @Override + public InputSource resolveEntity(String publicId, String systemId) { + return new InputSource(new BufferedInputStream(this.getClass().getClassLoader().getResourceAsStream("dtd/xhtml1-transitional-macaco-edit.dtd"))); + } + + }); + reader.setContentHandler(new DefaultHandler() { + + @Override + public void startElement(String namespaceUri, String strippedName, String tagName, Attributes attributes) throws SAXException { + if ("script".equalsIgnoreCase(tagName)) { + throw new SAXException(String.format("Tag not allowed: %s.", tagName)); + } + for (int a = 0; attributes.getLength() > a; a++) { + if (attributes.getLocalName(a).toLowerCase().startsWith("on")) { + throw new SAXException(String.format("Attribute not allowed: %s.", attributes.getLocalName(a))); + } + } + } + + }); + reader.setErrorHandler(new ErrorHandler() { + + @Override + public void error(SAXParseException exception) throws SAXException { + throw new SAXException(exception); + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw new SAXException(exception); + } + + @Override + public void warning(SAXParseException exception) throws SAXException { + throw new SAXException(exception); + } + + }); + // + reader.parse(new InputSource(new ByteArrayInputStream(fakeXhtmlPageContent.toString().getBytes(MConstants.DEFAULT_CHARSET)))); + } + catch (ParserConfigurationException exception) { // cannot happen + } + catch (SAXException exception) { + throw new MXhtmlUnsafeStringException("Invalid 'x': unsafe tags or attributes inside.", exception); + } + catch (UnsupportedEncodingException exception) { // cannot happen + } + catch (IOException exception) { // cannot happen; put here not to bypass UnsupportedEncodingException + } + // Also convert named entities to numeric entities. + return MText.getXhtmlNumericEntitiesString(text); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MTextException.java b/10.x/src/main/java/com/marcozanon/macaco/text/MTextException.java new file mode 100644 index 0000000..af1a181 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MTextException.java @@ -0,0 +1,31 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +import com.marcozanon.macaco.MException; + +public abstract class MTextException extends MException { + + /* */ + + public MTextException() { + super(); + } + + public MTextException(String message) { + super(message); + } + + public MTextException(Throwable error) { + super(error); + } + + public MTextException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationException.java b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationException.java new file mode 100644 index 0000000..c8654cd --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationException.java @@ -0,0 +1,29 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +public abstract class MTranslationException extends MTextException { + + /* */ + + public MTranslationException() { + super(); + } + + public MTranslationException(String message) { + super(message); + } + + public MTranslationException(Throwable error) { + super(error); + } + + public MTranslationException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationFileParsingException.java b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationFileParsingException.java new file mode 100644 index 0000000..bfc3f5b --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationFileParsingException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +@SuppressWarnings("serial") +public class MTranslationFileParsingException extends MTranslationException { + + /* */ + + public MTranslationFileParsingException() { + super(); + } + + public MTranslationFileParsingException(String message) { + super(message); + } + + public MTranslationFileParsingException(Throwable error) { + super(error); + } + + public MTranslationFileParsingException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationValueNotFoundException.java b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationValueNotFoundException.java new file mode 100644 index 0000000..1323074 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslationValueNotFoundException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +@SuppressWarnings("serial") +public class MTranslationValueNotFoundException extends MTranslationException { + + /* */ + + public MTranslationValueNotFoundException() { + super(); + } + + public MTranslationValueNotFoundException(String message) { + super(message); + } + + public MTranslationValueNotFoundException(Throwable error) { + super(error); + } + + public MTranslationValueNotFoundException(String message, Throwable error) { + super(message, error); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MTranslator.java b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslator.java new file mode 100644 index 0000000..934cdf8 --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MTranslator.java @@ -0,0 +1,200 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +import com.marcozanon.macaco.MConstants; +import com.marcozanon.macaco.MObject; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.UnsupportedEncodingException; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Locale; + +public class MTranslator extends MObject { + + protected Locale basicLocale = null; + + protected LinkedHashMap<String, LinkedHashMap<String, String>> messages = new LinkedHashMap<String, LinkedHashMap<String, String>>(); + + /* */ + + public MTranslator(Locale basicLocale) { + super(); + // + if (null == basicLocale) { + throw new IllegalArgumentException("Invalid 'basicLocale': null."); + } + // + this.basicLocale = basicLocale; + } + + public MTranslator(Locale basicLocale, LinkedHashMap<String, LinkedHashMap<String, String>> messages) { + this(basicLocale); + // + this.messages = messages; + } + + public MTranslator(Locale basicLocale, Path file) throws MTranslationFileParsingException { + this(basicLocale); + // + this.parseFile(file); + } + + @Override + public MTranslator clone() { + return new MTranslator(this.getBasicLocale(), this.getMessages()); + } + + /* Locale. */ + + protected Locale getBasicLocale() { + return this.basicLocale; + } + + /* String management. */ + + protected LinkedHashMap<String, LinkedHashMap<String, String>> getMessages() { + return this.messages; + } + + public void parseFile(Path file) throws MTranslationFileParsingException { + if (null == file) { + throw new IllegalArgumentException("Invalid 'file': null."); + } + // + try ( + LineNumberReader buffer = new LineNumberReader(new InputStreamReader(new FileInputStream(file.toFile()), MConstants.DEFAULT_CHARSET)); + ) { + String message = null; + String line = null; + synchronized (this.getMessages()) { + while (true) { + try { + line = buffer.readLine(); + } + catch (IOException exception) { + throw new MTranslationFileParsingException("Could not read file.", exception); + } + // + if (null == line) { + break; + } + // + line = line.trim(); + // + if (MText.isEmpty(line)) { + message = null; + continue; + } + else if ((line.startsWith("#")) || (line.startsWith(";"))) { + continue; + } + else if (null == message) { + message = line; + if (this.getMessages().containsKey(message)) { + throw new MTranslationFileParsingException(String.format("Invalid line: %s: duplicated message: %s.", buffer.getLineNumber(), message)); + } + // + this.getMessages().put(message, new LinkedHashMap<String, String>()); + } + else { + int a = line.indexOf("="); + if (-1 == a) { + throw new MTranslationFileParsingException(String.format("Invalid line: %s: string malformed: %s.", buffer.getLineNumber(), line)); + } + // + String localeRepresentation = line.substring(0, a).trim(); + String translation = line.substring(a + 1).trim(); + if (this.getMessages().get(message).containsKey(localeRepresentation)) { + throw new MTranslationFileParsingException(String.format("Invalid line: %s: duplicated translation for locale: %s.", buffer.getLineNumber(), message)); + } + // + this.getMessages().get(message).put(localeRepresentation, translation); + } + } + } + } + catch (FileNotFoundException exception) { + throw new MTranslationFileParsingException("Could not open file.", exception); + } + catch (UnsupportedEncodingException exception) { // cannot happen + } + catch (IOException exception) { + throw new MTranslationFileParsingException("Could not open / close file.", exception); + } + } + + public void clear() { + synchronized (this.getMessages()) { + this.getMessages().clear(); + } + } + + public String t(String message, Locale locale) { + return this.getLenientTranslation(message, locale); + } + + public String getLenientTranslation(String message, Locale locale) { + String translation = null; + // + try { + translation = this.getTranslation(message, locale, false); + } + catch (MTranslationValueNotFoundException exception) { // cannot happen + } + // + return translation; + } + + public String getStrictTranslation(String message, Locale locale) throws MTranslationValueNotFoundException { + return this.getTranslation(message, locale, true); + } + + protected String getTranslation(String message, Locale locale, boolean strictMode) throws MTranslationValueNotFoundException { + if (MText.isBlank(message)) { + throw new IllegalArgumentException("Invalid 'message': null or empty."); + } + if (null == locale) { + throw new IllegalArgumentException("Invalid 'locale': null."); + } + // + if (!this.getMessages().containsKey(message)) { + if (strictMode) { + throw new MTranslationValueNotFoundException(String.format("Invalid 'message': %s: not available.", message)); + } + // + return message; + } +/* + if (this.getBasicLocale().equals(locale)) { + return message; + } +*/ + LinkedHashMap<String, String> messageTranslations = this.getMessages().get(message); + String localeRepresentation = locale.toString(); + if (!messageTranslations.containsKey(localeRepresentation)) { + if (strictMode) { + throw new MTranslationValueNotFoundException(String.format("Invalid 'locale': %s: translation not available for message: %s.", localeRepresentation, message)); + } + // + String localeFallbackRepresentation = locale.getLanguage(); + // + if (!messageTranslations.containsKey(localeFallbackRepresentation)) { + return message; + } + // + return messageTranslations.get(localeFallbackRepresentation); + } + // + return messageTranslations.get(localeRepresentation); + } + +} diff --git a/10.x/src/main/java/com/marcozanon/macaco/text/MXhtmlUnsafeStringException.java b/10.x/src/main/java/com/marcozanon/macaco/text/MXhtmlUnsafeStringException.java new file mode 100644 index 0000000..728e83e --- /dev/null +++ b/10.x/src/main/java/com/marcozanon/macaco/text/MXhtmlUnsafeStringException.java @@ -0,0 +1,30 @@ +/** + * Macaco + * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>. + * See LICENSE for details. + */ + +package com.marcozanon.macaco.text; + +@SuppressWarnings("serial") +public class MXhtmlUnsafeStringException extends MTextException { + + /* */ + + public MXhtmlUnsafeStringException() { + super(); + } + + public MXhtmlUnsafeStringException(String message) { + super(message); + } + + public MXhtmlUnsafeStringException(Throwable error) { + super(error); + } + + public MXhtmlUnsafeStringException(String message, Throwable error) { + super(message, error); + } + +}