All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sta.mlogger.MLogEntry Maven / Gradle / Ivy

There is a newer version: 1.11
Show newest version

package com.sta.mlogger;

import java.util.Date;
import java.util.Properties;
// import java.util.StringTokenizer;

import java.text.SimpleDateFormat;

/**
 * 

Title: MLogEntry

*

Description: Allgemeiner Logger-Eintrag zur Registration eines "realen" * Loggers in Verbindung mit minimalem und maximalem LogLevel beim Logger. * Nur die Nachrichten, deren LogLevel gr??er oder gleich dem minimalen * LogLevel sowie kleiner oder gleich dem maximalen LogLevel sind, sollen * formatiert und ausgegeben werden. *

*

Copyright: Copyright (c) 2002-2005, 2014, 2017-2019

*

Company: >StA-Soft<

* @author StA * @version 1.4 */ public abstract class MLogEntry { /** * Log-Level, falls nur Fehler ausgegeben werden sollen. */ public static final int ERROR = 0; /** * Log-Level, falls Fehler und Warnungen ausgegeben werden sollen. */ public static final int WARNING = 1; /** * Log-Level, falls Fehler, Warnungen und Infos ausgegeben werden sollen. */ public static final int INFO = 2; /** * Log-Level, falls Fehler, Warnungen, Infos und Nachrichten ausgegeben * werden sollen. */ public static final int MESSAGE = 3; /** * Log-Level, falls Fehler, Warnungen, Infos, Nachrichten und Debug-Meldungen * ausgegeben werden sollen. */ public static final int DEBUG = 4; /** * Log-Level, falls alles ausgegeben werden soll. */ public static final int ALL = 5; /** * Nicht abschneiden. */ public static final int NO_CUT = 0; /** * Links abschneiden, also ab einer bestimmten Position bis zum Ende alles * ?bernehmen. */ public static final int CUT_LEFT = 1; /** * Rechts abschneiden, also vom Beginn an alles bis zur angegebenen maximalen * L?nge ?bernehmen. */ public static final int CUT_RIGHT = 2; /** * K?rzere Texte auf maximale L?nge auff?llen, l?ngere Texte komplett * ?bernehmen und maximale L?nge auf Textl?nge setzen. */ public static final int CUT_DYNAMIC = 3; /** * Speziell f?r Package-Namen: Namen von links beginnend mit jeweils einem * Buchstaben abk?rzen, falls der Text l?nger als die maximale L?nge ist. * Sollte der Text danach immer noch zu lang sein, dann ggf. entsprechend der * obigen Cut-Modi abschneiden (Oder-Verkn?pfung dieser Konstante mit * Cut-Modi). */ public static final int USE_SHORTCUT = 8; //=========================================================================== /** * Funktionales Interface, um einen Integer zu setzen. * (kann ersetzt werden, falls es das schon standardm??ig gibt) */ protected interface SetInt { /** * Integer setzen. * @param i Integer-Wert */ void setInt(int i); } /** * Minimaler LogLevel. Nachrichten mit geringerem LogLevel werden nicht * ausgegeben. * @see #ERROR * @see #WARNING * @see #INFO * @see #MESSAGE * @see #DEBUG * @see #ALL */ private int myMinLevel; /** * Maximaler LogLevel. Nachrichten mit h?herem LogLevel werden nicht * ausgegeben. * @see #ERROR * @see #WARNING * @see #INFO * @see #MESSAGE * @see #DEBUG * @see #ALL */ private int myMaxLevel; /** * Zu verwendendes Datumsformat, Standard: "yyyy-MM-dd". * @see #myTimeFormat * @see #myMLogFormat */ protected String myDateFormat = "yyyy-MM-dd"; /** * Zu verwendendes Zeitformat, Standard: "HH:mm:ss". * @see #myDateFormat * @see #myMLogFormat */ protected String myTimeFormat = "HH:mm:ss"; /** * Logging-Format, Standard: "%date %time %lev (%loc): %msg". * @see #myDateFormat * @see #myTimeFormat */ protected String myMLogFormat = "%date %time %lev (%loc): %msg"; // protected String myMLogFormat = "%date %time %thread %loc %source %line %lev %msg"; /** * Namen f?r die Logging-Level. */ protected String[] myLevelNames = { "Error", "Warning", "Info", "Message", "Debug", "All" }; /** * Falls MLogEntry f?r Thread kann die Ausgabe ?ber weitere MLogEntries * unterbunden werden, wenn das Flag auf true gesetzt wird. */ protected boolean myBreakAfterOutput = false; /** * Cut-Mode f?r Location-Angabe. Standard: nicht abschneiden. */ protected int myCutMode4Location = NO_CUT; /** * Maximale L?nge der Location-Angabe. Standard: 0. */ protected int myMaxLen4Location = 0; /** * Cut-Mode f?r Thread-Namen. Standard: nicht abschneiden. */ protected int myCutMode4ThreadName = NO_CUT; /** * Maximale L?nge der Thread-Namen. Standard: 0. */ protected int myMaxLen4ThreadName = 0; //=========================================================================== /** * Initialisierung eines MLogEntry. * @param pMinLevel minimaler LogLevel * @param pMaxLevel maximaler LogLevel * @see #MLogEntry(int) * @see #MLogEntry() */ public MLogEntry(int pMinLevel, int pMaxLevel) { myMinLevel = pMinLevel; myMaxLevel = pMaxLevel; } /** * Initialisierung eines MLogEntry mit MinLevel = ERROR. * @param pMaxLevel maximaler LogLevel * @see #MLogEntry(int, int) * @see #MLogEntry() */ public MLogEntry(int pMaxLevel) { this(ERROR, pMaxLevel); } /** * Initialisierung eines MLogEntry mit MinLevel = ERROR und MaxLevel = ALL. * @see #MLogEntry(int, int) * @see #MLogEntry(int) */ public MLogEntry() { this(ERROR, ALL); } //=========================================================================== /** * Minimalen LogLevel setzen. * @param pMinLevel minimaler LogLevel */ public void setMinLevel(int pMinLevel) { myMinLevel = pMinLevel; } /** * Minimalen LogLevel ermitteln. * @return minimaler LogLevel */ public int getMinLevel() { return myMinLevel; } /** * Maximalen LogLevel setzen. * @param pMaxLevel maximaler LogLevel */ public void setMaxLevel(int pMaxLevel) { myMaxLevel = pMaxLevel; } /** * Maximalen LogLevel ermitteln. * @return maximaler LogLevel */ public int getMaxLevel() { return myMaxLevel; } /** * Datumsformat vorgeben. * @param pDateFormat Datumsformat */ public void setDateFormat(String pDateFormat) { myDateFormat = pDateFormat; } /** * Datumsformat ermitteln. * @return Datumsformat */ public String getDateFormat() { return myDateFormat; } /** * Zeitformat vorgeben. * @param pTimeFormat Zeitformat */ public void setTimeFormat(String pTimeFormat) { myTimeFormat = pTimeFormat; } /** * Zeitformat ermitteln. * @return Zeitformat */ public String getTimeFormat() { return myTimeFormat; } /** * Logging-Format vorgeben. * @param pMLogFormat Logging-Format */ public void setMLogFormat(String pMLogFormat) { myMLogFormat = pMLogFormat; } /** * Logging-Format ermitteln. * @return Logging-Format */ public String getMLogFormat() { return myMLogFormat; } /** * Level-Namen "beschneiden". */ public void trimLevelNames() { for (int i = 0; i < myLevelNames.length; i++) { myLevelNames[i] = myLevelNames[i].trim(); } } /** * Level-Namen vorbereiten. */ public void prepLevelNames() { int len = 0; for (int i = 0; i < myLevelNames.length; i++) { if (myLevelNames[i].length() > len) { len = myLevelNames[i].length(); } } for (int i = 0; i < myLevelNames.length; i++) { StringBuilder sb = new StringBuilder(myLevelNames[i]); while (sb.length() < len) { sb.append(' '); } myLevelNames[i] = sb.toString(); } } /** * Break-Flag setzen. * @param bao true: weitere Ausgabe unterbinden, false: weiter ausgeben */ public void setBreakAfterOutput(boolean bao) { myBreakAfterOutput = bao; } /** * Break-Flag ermitteln. * @return true: weitere Ausgabe unterbinden, false: weiter ausgeben */ public boolean getBreakAfterOutput() { return myBreakAfterOutput; } /** * Cut-Mode f?r Location-Angabe vorgeben. * @param cutmode Cut-Mode f?r Location-Angabe */ public void setCutMode4Location(int cutmode) { myCutMode4Location = cutmode; } /** * Cut-Mode f?r Location-Angabe ermitteln. * @return Cut-Mode f?r Location-Angabe */ public int getCutMode4Location() { return myCutMode4Location; } /** * Maximale L?nge der Location-Angabe vorgeben. * @param maxlen maximale L?nge der Location-Angabe */ public void setMaxLen4Location(int maxlen) { myMaxLen4Location = maxlen; } /** * Maximale L?nge der Location-Angabe ermitteln. * @return maximale L?nge der Location-Angabe */ public int getMaxLen4Location() { return myMaxLen4Location; } /** * Cut-Mode f?r Thread-Namen vorgeben. * @param cutmode Cut-Mode f?r Thread-Namen */ public void setCutMode4ThreadName(int cutmode) { myCutMode4ThreadName = cutmode; } /** * Cut-Mode f?r Thread-Namen ermitteln. * @return Cut-Mode f?r Thread-Namen */ public int getCutMode4ThreadName() { return myCutMode4ThreadName; } /** * Maximale L?nge der Thread-Namen vorgeben. * @param maxlen maximale L?nge der Thread-Namen */ public void setMaxLen4ThreadName(int maxlen) { myMaxLen4ThreadName = maxlen; } /** * Maximale L?nge der Thread-Namen ermitteln. * @return maximale L?nge der Thread-Namen */ public int getMaxLen4ThreadName() { return myMaxLen4ThreadName; } //--------------------------------------------------------------------------- /** * Initialisierung mit Properties. * @param p Properties */ public void init(Properties p) { String minlevel = p.getProperty("MinLevel"); if (minlevel != null) { setMinLevel(Integer.parseInt(minlevel)); } String maxlevel = p.getProperty("MaxLevel"); if (maxlevel != null) { setMaxLevel(Integer.parseInt(maxlevel)); } String dateformat = p.getProperty("DateFormat"); if (dateformat != null) { setDateFormat(dateformat); } String timeformat = p.getProperty("TimeFormat"); if (timeformat != null) { setTimeFormat(timeformat); } String mlogformat = p.getProperty("MLogFormat"); if (mlogformat != null) { setMLogFormat(mlogformat); } String trimlevelnames = p.getProperty("TrimLevelNames"); if ("1".equals(trimlevelnames)) { trimLevelNames(); } String preplevelnames = p.getProperty("PrepLevelNames"); if ("1".equals(preplevelnames)) { prepLevelNames(); } String breakafteroutput = p.getProperty("BreakAfterOutput"); if (breakafteroutput != null) { setBreakAfterOutput("1".equals(breakafteroutput)); } String cutmode4location = p.getProperty("CutMode4Location"); if (cutmode4location != null) { setCutMode4Location(Integer.parseInt(cutmode4location)); } String maxlen4location = p.getProperty("MaxLen4Location"); if (maxlen4location != null) { setMaxLen4Location(Integer.parseInt(maxlen4location)); } String cutmode4threadname = p.getProperty("CutMode4ThreadName"); if (cutmode4threadname != null) { setCutMode4ThreadName(Integer.parseInt(cutmode4threadname)); } String maxlen4threadname = p.getProperty("MaxLen4ThreadName"); if (maxlen4threadname != null) { setMaxLen4ThreadName(Integer.parseInt(maxlen4threadname)); } } //--------------------------------------------------------------------------- /* * * String-Vergleich f?r equals * @param s erster String * @param d zweiter String * @return true, falls (s und d gleich null) oder (s == d). */ /* protected boolean eqString(String s, String d) { if ((s == null) && (d != null)) { return false; } if ((s != null) && (!s.equals(d))) { return false; } return true; } */ /* * * Standard-Equals-Methode * @param other Objekt, mit dem das aktuelle vergleichen werden soll * @return true falls "logisch gleich" */ /* public boolean equals(Object other) { // Standard-Implementation if (this == other) { return true; } if (other == null) { return false; } if (other.getClass() != getClass()) { return false; } // Spezifische Implementation MLogEntry le = (MLogEntry) other; return (myMinLevel == le.getMinLevel()) && (myMaxLevel == le.getMaxLevel()) && eqString(myDateFormat, le.getDateFormat()) && eqString(myTimeFormat, le.getTimeFormat()) && eqString(myMLogFormat, le.getMLogFormat()); } */ //--------------------------------------------------------------------------- /** * Pr?fen, ob Nachrichten im ?bergebenen LogLevel ausgegeben werden sollen. * @param pLocation Ort, vollst?ndig (Package + Methodenname) * @param pLevel LogLevel, auf den gepr?ft werden soll * @return true, falls die Nachricht(en) ausgegeben werden soll */ public boolean checkLevel(String pLocation, int pLevel) { return (pLevel >= myMinLevel) && (pLevel <= myMaxLevel); } /** * Optimierte Methode zum K?rzen von hierarchisch strukturierten Namen, * konkret Namen von Package.Klasse.Methode. * @param s ggf. zu k?rzender Text * @param delim strukturbildendes Trennzeichen * @param lastdelimcnt Anzahl der letzten zu erhaltenden Trennzeichen bzw. * Textteile * @param maxlen maximale bzw. Ziel-L?nge * @return ggf. gek?rzter Text */ public String abbreviate(String s, char delim, int lastdelimcnt, int maxlen) { if (s == null) { return s; } // Falls maxlen noch nicht erreicht, muss nichts gek?rzt werden int len = s.length(); if (len <= maxlen) { return s; } // von hinten lastdelimcnt ?berlesen int lastdelim = len; for (int i = 0; i < lastdelimcnt; i++) { int newlastdelim = s.lastIndexOf(delim, lastdelim - 1); if ((newlastdelim < 0) || (newlastdelim == lastdelim)) { return s; } lastdelim = newlastdelim; } // restliche L?nge ab lastdelim // int restlen = len - lastdelim; // sicherheitshalber if (lastdelim <= 0) { return s; } // jetzt ist klar, dass vom Beginn an gek?rzt werden soll int i = 0; StringBuilder sb = new StringBuilder(); while (i < lastdelim) { char ch; do { ch = s.charAt(i); i++; sb.append(ch); } while (ch == delim); while (s.charAt(i) != delim) { i++; } if (i == lastdelim) { sb.append(s.substring(lastdelim)); return sb.toString(); // break; } // if (sb.length() + (lastdelim - i) + restlen <= maxlen) if (sb.length() + len - i <= maxlen) { sb.append(s.substring(i)); return sb.toString(); } i++; sb.append(delim); } // jetzt ist es egal, ob maxlen erreicht sb.append(s.substring(lastdelim)); return sb.toString(); } /* * * Hier soll das erste noch nicht abgek?rzte Package gefunden und abgek?rzt * werden. Abgek?rzt bedeutet: 1 Zeichen, nicht abgek?rzt: mehr als 1 Zeichen. * @param s Packages, mit "." getrennt, * @return Packages, ggf. mit zus?tzlichem abgek?rzten Package */ /* protected String makeShortCut(String s) { if (s == null) { return null; } int li = s.lastIndexOf('.'); if (li <= 0) { return s; } li = s.lastIndexOf('.', li - 1); if (li <= 0) { return s; } StringBuilder sb = new StringBuilder(); StringTokenizer st = new StringTokenizer(s.substring(0, li), "."); boolean copy = false; while (st.hasMoreTokens()) { String pack = st.nextToken(); if (!copy) { if (pack.length() > 1) { sb.append(pack.charAt(0)); copy = true; } else { sb.append(pack); } } else { sb.append(pack); } sb.append('.'); } sb.append(s.substring(li + 1)); return sb.toString(); } */ /** * Universelle Cut-Methode, Abschneiden entsprechend Cut-Mode. * @param s Text * @param cutmode Cut-Mode * @param maxlen max. L?nge * @param si funktionales Interface um einen Integer zu setzen (zu ?ndern), * dabei geht es darum, bei Bedarf die maximale L?nge zu ?ndern * @return ggf. lt. Cut-Mode abgeschnittener Text */ protected String cut(String s, int cutmode, int maxlen, SetInt si) { if (s == null) { return s; } if (cutmode == NO_CUT) { return s; } else if (cutmode == CUT_LEFT) { return s.length() > maxlen ? s.substring(s.length() - maxlen) : s; } else if (cutmode == CUT_RIGHT) { return s.length() > maxlen ? s.substring(0, maxlen) : s; } else if (cutmode == CUT_DYNAMIC) { int len = s.length(); if (len > maxlen) { // maxlen = len; if (si != null) { si.setInt(len); } return s; } if (len == maxlen) { return s; } StringBuilder sb = new StringBuilder(s); while (sb.length() < maxlen) { sb.append(" "); } return sb.toString(); } else if (cutmode == USE_SHORTCUT) { return abbreviate(s, '.', 2, maxlen); } else if (cutmode == USE_SHORTCUT + CUT_DYNAMIC) { return cut(abbreviate(s, '.', 2, maxlen), CUT_DYNAMIC, maxlen, si); } return s; } /** * Formatierung einer Nachricht. Derzeit werden folgende Texte ersetzt: *
    *
  • %% = %
  • *
  • %date = Datum lt. Datumsformat
  • *
  • %time = Zeit lt. Zeitformat
  • *
  • %thread = Name des aktuellen Threads
  • *
  • %location = Ort, vollst?ndig (Package + Klassenname + Methodenname)
  • *
  • %loc = Ort, gek?rzt (Klassenname + Methodenname, also ohne Package)
  • *
  • %lev = LogLevel in Textdarstellung
  • *
  • %msg = die eigentliche Nachricht
  • *
  • %mdc = Werte von MDC-Feldern
  • *
* Alle anderen Zeichen werden 1:1 ?bernommen. * Bei %location und %loc kann in Klammern die max. L?nge angegeben werden, * in diesem Fall erfolgt ein K?rzen der Package-Namen ohne Dynamik. * Beispiel: %location(20) * ... bedeutet dann, dass auf 20 Zeichen abgek?rzt werden soll. * Ferner kann durch Komma getrennt der Cut-Mode angegeben werden. * Beispiel: %location(20,11) * ... bedeutet dann, dass auf 20 Zeichen abgek?rzt werden soll, wobei die * verbleibende L?nge die neue max. L?nge wird, falls diese gr??er als die * aktuelle max. L?nge ist. * Diese Methode kann ?berschrieben werden, um etwa spezielle Formatierungen * (z. B. HTML: Zeilenumbruch erg?nzen) vorzunehmen. * @param pLocation Ort, vollst?ndig (Package + Methodenname) * @param pLevel LogLevel * @param pStrg Nachricht * @return Formatierte Nachricht */ protected String format(String pLocation, int pLevel, String pStrg) { Date d = new Date(); SimpleDateFormat sdf = new SimpleDateFormat(myDateFormat); SimpleDateFormat stf = new SimpleDateFormat(myTimeFormat); StringBuilder sb = new StringBuilder(); int j = pLocation.lastIndexOf('.', pLocation.lastIndexOf('.') - 1); String loc = (j >= 0) && (j < pLocation.length() - 1) ? pLocation.substring(j + 1) : pLocation; int len = myMLogFormat.length(); int i = 0; while (i < len) { char ch = myMLogFormat.charAt(i); if ((ch == '%') && (len - i > 1)) { i += 1; String s = myMLogFormat.substring(i); if (s.charAt(0) == '%') { sb.append(ch); i++; } else if (s.startsWith("date")) { sb.append(sdf.format(d)); i += 4; } else if (s.startsWith("time")) { sb.append(stf.format(d)); i += 4; } else if (s.startsWith("thread")) { String threadname = Thread.currentThread().getName(); if (threadname != null) { sb.append(cut(threadname.replace(" ", "_"), myCutMode4ThreadName, myMaxLen4ThreadName, this::setMaxLen4ThreadName)); } i += 6; } else if (s.startsWith("location(") && s.contains(")")) { int i1 = s.indexOf(")"); String s1 = s.substring(9, i1); int i2 = s1.indexOf(","); int cutmode = USE_SHORTCUT; if (i2 >= 0) { String s2 = s1.substring(i2 + 1); s1 = s1.substring(0, i2); cutmode = Integer.valueOf(s2); } int maxlen = Integer.valueOf(s1); if (maxlen > myMaxLen4Location) { myMaxLen4Location = maxlen; } sb.append(cut(pLocation, cutmode, myMaxLen4Location, this::setMaxLen4Location)); i += i1 + 1; } else if (s.startsWith("location")) { sb.append(cut(pLocation, myCutMode4Location, myMaxLen4Location, this::setMaxLen4Location)); i += 8; } else if (s.startsWith("loc(") && s.contains(")")) { int i1 = s.indexOf(")"); String s1 = s.substring(4, i1); int maxlen = Integer.valueOf(s1); sb.append(cut(loc, USE_SHORTCUT, maxlen, this::setMaxLen4Location)); i += i1 + 1; } else if (s.startsWith("loc")) { sb.append(cut(loc, myCutMode4Location, myMaxLen4Location, this::setMaxLen4Location)); i += 3; } /* else if (s.startsWith("source")) { i += 6; } else if (s.startsWith("line")) { i += 4; } */ else if (s.startsWith("lev")) { if ((pLevel >= 0) && (pLevel < myLevelNames.length)) { sb.append(myLevelNames[pLevel]); } else { sb.append("<%LevelError%>"); } i += 3; } else if (s.startsWith("msg")) { sb.append(pStrg); i += 3; } else if (s.startsWith("mdc(") && s.contains(")")) { int i1 = s.indexOf(")"); String key = s.substring(4, i1); Object value = MThreadContext.getValue(key); sb.append(value != null ? value.toString() : ""); i += i1 + 1; } else { sb.append("<%FormatError%>"); } } else { sb.append(ch); i++; } } return sb.toString(); } //--------------------------------------------------------------------------- /** * Text ausgeben. * Diese Methode muss ?berschrieben werden. * Der Aufruf erfolgt nur aus MLogEntries heraus. * @param pStrg der auszugebende Text (bereits formatiert) * @see #println(String, int, String) */ protected abstract void println(String pStrg); /** * Text entsprechend vorgegebenem LogLevel formatieren und ausgeben. * Falls die Nachricht entsprechend dem LogLevel ausgegeben werden soll, * formatiert diese Standard-Implementation den Text und ruft println(String) * auf. Diese Methode muss in der Regel nicht ?berschrieben werden. Der * Aufruf erfolgt aus MLogger. * @param pLocation Ort (z. B. Methodenname o. ?.) * @param pLevel vorgegebener LogLevel * @param pStrg der auszugebende Text (nicht formatiert) * @see #println(String) */ public void println(String pLocation, int pLevel, String pStrg) { if (checkLevel(pLocation, pLevel)) { println(format(pLocation, pLevel, pStrg)); } } /** * Falls Text entsprechend LogLevel ausgegeben werden soll, wird der Text * erstellt und ausgegeben. * @param pLocation Ort (z. B. Methodenname o. ?.) * @param pLevel vorgegebener LogLevel * @param mb Message-Builder */ public void println(String pLocation, int pLevel, IMessageBuilder mb) { if (checkLevel(pLocation, pLevel)) { println(format(pLocation, pLevel, mb.getMessage())); } } //--------------------------------------------------------------------------- /** * Text ausgeben. * Diese Methode muss ?berschrieben werden. * Der Aufruf erfolgt nur aus MLogEntries heraus. * @param pStrg der auszugebende Text (bereits formatiert) * @see #println(String, int, String) */ protected abstract void print(String pStrg); /** * Text entsprechend vorgegebenem LogLevel formatieren und ausgeben. * Falls die Nachricht entsprechend dem LogLevel ausgegeben werden soll, * formatiert diese Standard-Implementation den Text und ruft println(String) * auf. Diese Methode muss in der Regel nicht ?berschrieben werden. Der * Aufruf erfolgt aus MLogger. * @param pLocation Ort (z. B. Methodenname o. ?.) * @param pLevel vorgegebener LogLevel * @param pStrg der auszugebende Text (nicht formatiert) * @see #println(String) */ public void print(String pLocation, int pLevel, String pStrg) { if (checkLevel(pLocation, pLevel)) { print(format(pLocation, pLevel, pStrg)); } } /** * Falls Text entsprechend LogLevel ausgegeben werden soll, wird der Text * erstellt und ausgegeben. * @param pLocation Ort (z. B. Methodenname o. ?.) * @param pLevel vorgegebener LogLevel * @param mb Message-Builder */ public void print(String pLocation, int pLevel, IMessageBuilder mb) { if (checkLevel(pLocation, pLevel)) { print(format(pLocation, pLevel, mb.getMessage())); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy