diff --git a/superx/edit/kern/etl_manager.jsp b/superx/edit/kern/etl_manager.jsp new file mode 100644 index 0000000..923fb56 --- /dev/null +++ b/superx/edit/kern/etl_manager.jsp @@ -0,0 +1,744 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<%@ page import="java.io.File" %> +<%@ page import="java.io.BufferedReader" %> +<%@ page import="java.io.InputStreamReader" %> +<%@ page import="java.lang.management.ManagementFactory" %> +<%@ page import="java.sql.Connection" %> +<%@ page import="java.sql.PreparedStatement" %> +<%@ page import="java.sql.ResultSet" %> +<%@ page import="java.sql.SQLException" %> +<%@ page import="java.sql.Statement" %> +<%@ page import="java.text.SimpleDateFormat" %> +<%@ page import="java.util.Date" %> +<%@ page import="java.util.List" %> +<%@ page import="java.util.LinkedHashMap" %> +<%@ page import="java.util.Map" %> +<%@ page import="javax.naming.InitialContext" %> +<%@ page import="javax.sql.DataSource" %> +<%@ page import="jakarta.servlet.ServletContext" %> +<%@ page import="javax.xml.parsers.DocumentBuilder" %> +<%@ page import="javax.xml.parsers.DocumentBuilderFactory" %> +<%@ page import="org.w3c.dom.Document" %> +<%! +private String html(Object value) { + if (value == null) return ""; + String s = String.valueOf(value); + return s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(String.valueOf((char)34), """) + .replace("'", "'"); +} + +private String tag(String text, String type) { + String cls = "is-light"; + if ("ok".equals(type)) cls = "is-success is-light"; + else if ("warn".equals(type)) cls = "is-warning is-light"; + else if ("error".equals(type)) cls = "is-danger is-light"; + else if ("info".equals(type)) cls = "is-info is-light"; + return "" + html(text) + ""; +} + +private String formatBytes(long bytes) { + if (bytes < 0) return "-"; + String[] units = {"B", "KB", "MB", "GB", "TB"}; + double value = (double) bytes; + int idx = 0; + while (value >= 1024.0 && idx < units.length - 1) { + value = value / 1024.0; + idx++; + } + if (idx == 0) return String.valueOf(bytes) + " " + units[idx]; + return String.format(java.util.Locale.GERMANY, "%.1f %s", value, units[idx]); +} + +private String nullText(Object value) { + if (value == null || String.valueOf(value).length() == 0) { + return "nicht sichtbar / nicht gesetzt"; + } + return html(value); +} + +private String jsString(Object value) { + if (value == null) return ""; + String s = String.valueOf(value); + return s.replace("\\", "\\\\") + .replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\"", "\\\""); +} + +private String joinPath(String base, String child) { + if (base == null || base.length() == 0) return null; + return new File(base, child).getPath(); +} + +private String fileCheckRow(String label, String path) { + StringBuilder sb = new StringBuilder(); + sb.append("").append(html(label)).append(""); + if (path == null || path.length() == 0) { + sb.append("nicht ermittelbar"); + sb.append("").append(tag("unbekannt", "warn")).append(""); + return sb.toString(); + } + File f = new File(path); + sb.append("").append(html(path)).append(""); + sb.append(""); + sb.append(f.exists() ? tag("vorhanden", "ok") : tag("fehlt", "warn")); + sb.append(""); + if (f.exists()) { + sb.append("lesbar: ").append(f.canRead() ? "ja" : "nein"); + sb.append(", schreibbar: ").append(f.canWrite() ? "ja" : "nein"); + if (f.isDirectory()) sb.append(", betretbar: ").append(f.canExecute() ? "ja" : "nein"); + } + sb.append(""); + return sb.toString(); +} + +private boolean tableExistsByRegclass(Connection con, String regclassName) { + PreparedStatement ps = null; + ResultSet rs = null; + try { + ps = con.prepareStatement("select to_regclass(?) is not null"); + ps.setString(1, regclassName); + rs = ps.executeQuery(); + return rs.next() && rs.getBoolean(1); + } catch (Exception e) { + return false; + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } +} + +private boolean hasDbVersionTable(Connection con) { + return tableExistsByRegclass(con, "db_version") || tableExistsByRegclass(con, "public.db_version"); +} + +private Connection connectionFromObject(Object obj, boolean requireDbVersion) throws Exception { + if (!(obj instanceof DataSource)) return null; + Connection con = null; + try { + con = ((DataSource) obj).getConnection(); + if (!requireDbVersion || hasDbVersionTable(con)) return con; + } catch (Exception e) { + try { if (con != null) con.close(); } catch (Exception ex) { } + throw e; + } + try { if (con != null) con.close(); } catch (Exception e) { } + return null; +} + +private Object springBean(Object wac, String beanName) { + try { + java.lang.reflect.Method getBean = wac.getClass().getMethod("getBean", String.class); + return getBean.invoke(wac, beanName); + } catch (Exception e) { + return null; + } +} + +private Connection connectionFromMap(Object obj, boolean requireDbVersion) throws Exception { + if (!(obj instanceof Map)) return null; + Map map = (Map) obj; + String[] preferredKeys = {"superx", "SuperX", "default", "hisinone", "eduetl"}; + for (int i = 0; i < preferredKeys.length; i++) { + if (map.containsKey(preferredKeys[i])) { + Connection c = connectionFromObject(map.get(preferredKeys[i]), requireDbVersion); + if (c != null) return c; + } + } + for (Object value : map.values()) { + Connection c = connectionFromObject(value, requireDbVersion); + if (c != null) return c; + } + return null; +} + +private Connection findSpringConnection(ServletContext application, boolean requireDbVersion) throws Exception { + Object wac = null; + try { + Class util = Class.forName("org.springframework.web.context.support.WebApplicationContextUtils"); + java.lang.reflect.Method m = util.getMethod("getWebApplicationContext", ServletContext.class); + wac = m.invoke(null, application); + } catch (Exception e) { + wac = null; + } + if (wac == null) return null; + + String[] beanNames = {"dataSources", "superxDataSource", "dataSource", "eduetlDataSource", "hisinoneDataSource"}; + for (int i = 0; i < beanNames.length; i++) { + Object bean = springBean(wac, beanNames[i]); + Connection c = connectionFromMap(bean, requireDbVersion); + if (c != null) return c; + c = connectionFromObject(bean, requireDbVersion); + if (c != null) return c; + } + + try { + java.lang.reflect.Method namesMethod = wac.getClass().getMethod("getBeanNamesForType", Class.class); + String[] names = (String[]) namesMethod.invoke(wac, DataSource.class); + if (names != null) { + for (int i = 0; i < names.length; i++) { + Object bean = springBean(wac, names[i]); + Connection c = connectionFromObject(bean, requireDbVersion); + if (c != null) return c; + } + } + } catch (Exception e) { } + return null; +} + +private Connection findJndiConnection(boolean requireDbVersion) throws Exception { + String[] jndiNames = { + "java:comp/env/jdbc/superx", + "java:comp/env/jdbc/eduetl", + "java:comp/env/jdbc/hisinone", + "jdbc/superx", + "jdbc/eduetl", + "jdbc/hisinone" + }; + InitialContext ctx = null; + try { ctx = new InitialContext(); } catch (Exception e) { ctx = null; } + if (ctx == null) return null; + for (int i = 0; i < jndiNames.length; i++) { + try { + Object obj = ctx.lookup(jndiNames[i]); + Connection c = connectionFromObject(obj, requireDbVersion); + if (c != null) return c; + } catch (Exception e) { } + } + return null; +} + +private Connection findServletContextConnection(ServletContext application, boolean requireDbVersion) throws Exception { + String[] attrNames = {"dataSources", "dataSource", "datasource", "superxDataSource", "eduetlDataSource", "javax.sql.DataSource"}; + for (int i = 0; i < attrNames.length; i++) { + try { + Object obj = application.getAttribute(attrNames[i]); + Connection c = connectionFromMap(obj, requireDbVersion); + if (c != null) return c; + c = connectionFromObject(obj, requireDbVersion); + if (c != null) return c; + } catch (Exception e) { } + } + return null; +} + +private Connection openDiagnosticConnection(ServletContext application) throws Exception { + // Erste Runde: bevorzugt eine Verbindung, in der db_version sichtbar ist. + Connection c = findSpringConnection(application, true); + if (c != null) return c; + c = findJndiConnection(true); + if (c != null) return c; + c = findServletContextConnection(application, true); + if (c != null) return c; + + // Zweite Runde: irgendeine verfügbare Verbindung, damit Diagnose nicht komplett ausfällt. + c = findSpringConnection(application, false); + if (c != null) return c; + c = findJndiConnection(false); + if (c != null) return c; + c = findServletContextConnection(application, false); + if (c != null) return c; + + throw new SQLException("Keine DataSource über Spring, JNDI oder ServletContext gefunden. REST-Checks und Dateisystem-Checks bleiben trotzdem verfügbar."); +} + +private String sqlColumnRows(Connection con, String tableSchema, String tableName) throws Exception { + PreparedStatement ps = null; + ResultSet rs = null; + StringBuilder sb = new StringBuilder(); + try { + ps = con.prepareStatement("select column_name, data_type from information_schema.columns where table_schema = ? and table_name = ? order by ordinal_position"); + ps.setString(1, tableSchema); + ps.setString(2, tableName); + rs = ps.executeQuery(); + while (rs.next()) { + sb.append("").append(html(rs.getString(1))).append("").append(html(rs.getString(2))).append(""); + } + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } + return sb.toString(); +} +private String readTrimmedFile(File f) { + if (f == null || !f.exists() || !f.isFile() || !f.canRead()) return ""; + StringBuilder sb = new StringBuilder(); + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(new java.io.FileInputStream(f), "UTF-8")); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + } catch (Exception e) { + return ""; + } finally { + try { if (br != null) br.close(); } catch (Exception e) { } + } + return sb.toString().trim(); +} + +private void disableXmlFeature(DocumentBuilderFactory factory, String feature) { + try { factory.setFeature(feature, false); } catch (Exception e) { } +} + +private String readModuleXmlVersion(File xmlFile) { + if (xmlFile == null || !xmlFile.exists() || !xmlFile.isFile() || !xmlFile.canRead()) return ""; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(false); + disableXmlFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd"); + disableXmlFeature(factory, "http://xml.org/sax/features/external-general-entities"); + disableXmlFeature(factory, "http://xml.org/sax/features/external-parameter-entities"); + try { factory.setXIncludeAware(false); } catch (Exception e) { } + try { factory.setExpandEntityReferences(false); } catch (Exception e) { } + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(xmlFile); + if (doc != null && doc.getDocumentElement() != null) { + String version = doc.getDocumentElement().getAttribute("version"); + if (version != null) return version.trim(); + } + } catch (Exception e) { + return ""; + } + return ""; +} + +private void addAvailableVersionInfo(Map map, String abbr, String xmlPath, String versionPath) { + if (abbr == null || abbr.trim().length() == 0) return; + String key = abbr.trim().toLowerCase(); + Map info = new LinkedHashMap(); + File xmlFile = xmlPath == null ? null : new File(xmlPath); + File versionFile = versionPath == null ? null : new File(versionPath); + String xmlVersion = readModuleXmlVersion(xmlFile); + String fileVersion = readTrimmedFile(versionFile); + info.put("xmlPath", xmlPath == null ? "" : xmlPath); + info.put("xmlVersion", xmlVersion == null ? "" : xmlVersion); + info.put("versionFilePath", versionPath == null ? "" : versionPath); + info.put("versionFileVersion", fileVersion == null ? "" : fileVersion); + if (xmlVersion != null && xmlVersion.length() > 0) { + info.put("version", xmlVersion); + info.put("source", "module_xml"); + } else if (fileVersion != null && fileVersion.length() > 0) { + info.put("version", fileVersion); + info.put("source", "version_file"); + } else { + info.put("version", ""); + info.put("source", ""); + } + map.put(key, info); +} + +private String availableVersionJson(Map map) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean first = true; + for (Object entryObj : map.entrySet()) { + Map.Entry entry = (Map.Entry) entryObj; + if (!first) sb.append(","); + first = false; + String key = String.valueOf(entry.getKey()); + Map info = (Map) entry.getValue(); + sb.append("\"").append(jsString(key)).append("\":{"); + String[] props = {"version", "source", "xmlVersion", "xmlPath", "versionFileVersion", "versionFilePath"}; + for (int i = 0; i < props.length; i++) { + if (i > 0) sb.append(","); + Object val = info.get(props[i]); + sb.append("\"").append(props[i]).append("\":\"").append(jsString(val == null ? "" : String.valueOf(val))).append("\""); + } + sb.append("}"); + } + sb.append("}"); + return sb.toString(); +} + +private String availableVersionRows(Map map) { + StringBuilder sb = new StringBuilder(); + for (Object entryObj : map.entrySet()) { + Map.Entry entry = (Map.Entry) entryObj; + String key = String.valueOf(entry.getKey()); + Map info = (Map) entry.getValue(); + sb.append("").append(html(key)).append(""); + sb.append("").append(info.get("xmlVersion") == null || String.valueOf(info.get("xmlVersion")).length() == 0 ? "nicht gefunden" : html(info.get("xmlVersion"))).append(""); + sb.append("").append(html(info.get("xmlPath"))).append(""); + sb.append("").append(info.get("versionFileVersion") == null || String.valueOf(info.get("versionFileVersion")).length() == 0 ? "nicht gefunden" : html(info.get("versionFileVersion"))).append(""); + sb.append("").append(html(info.get("versionFilePath"))).append(""); + sb.append("").append(info.get("version") == null || String.valueOf(info.get("version")).length() == 0 ? "nicht geliefert" : html(info.get("version"))).append(""); + } + return sb.toString(); +} +%> +<% +String diagGeneratedAt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(new Date()); +String contextPath = request.getContextPath(); +String servletRealPath = application.getRealPath("/"); +String canonicalRealPath = ""; +try { canonicalRealPath = servletRealPath == null ? "" : new File(servletRealPath).getCanonicalPath(); } +catch (Exception e) { canonicalRealPath = "nicht ermittelbar: " + e.getMessage(); } +String webInfPath = joinPath(servletRealPath, "WEB-INF"); +String edustorePath = joinPath(webInfPath, "conf/edustore"); +String dbPath = joinPath(edustorePath, "db"); +String modulePath = joinPath(dbPath, "module"); +String kernInstallPath = joinPath(dbPath, "install"); +String kernXmlPath = joinPath(kernInstallPath, "conf/kern.xml"); +String kernVersionFilePath = joinPath(kernInstallPath, "VERSION"); +String kennPath = joinPath(modulePath, "kenn"); +String kennXmlPath = joinPath(kennPath, "conf/kenn.xml"); +String kennRohdatenPath = joinPath(kennPath, "rohdaten"); +String kennSchluesselPath = joinPath(kennPath, "schluesseltabellen"); +File diskBase = servletRealPath == null ? null : new File(servletRealPath); +long diskTotal = diskBase == null ? -1 : diskBase.getTotalSpace(); +long diskFree = diskBase == null ? -1 : diskBase.getFreeSpace(); +long diskUsable = diskBase == null ? -1 : diskBase.getUsableSpace(); +boolean diskLow = diskUsable >= 0 && diskUsable < 1024L * 1024L * 1024L; +List jvmArgsList = ManagementFactory.getRuntimeMXBean().getInputArguments(); +StringBuilder jvmArgsBuilder = new StringBuilder(); +for (int i = 0; i < jvmArgsList.size(); i++) { + if (i > 0) jvmArgsBuilder.append(System.lineSeparator()); + jvmArgsBuilder.append(String.valueOf(jvmArgsList.get(i))); +} +String jvmArgs = jvmArgsBuilder.toString(); +boolean addOpensVisible = jvmArgs.indexOf("--add-opens=java.base/sun.net.www.protocol.jar=ALL-UNNAMED") >= 0; +String dbDiagnosticMessage = ""; +String dbInfoHtml = ""; +String jobLogTableTag = tag("unbekannt", "warn"); +String jobLogColumnsHtml = ""; +boolean dbDirectAvailable = false; +boolean jobLogExists = false; +boolean jobLogHasJobInstanceId = false; +boolean jobLogHasLog = false; +boolean jobLogHasStatus = false; +String dbVersionTableTag = tag("unbekannt", "warn"); +String dbVersionRowsHtml = ""; +String dbVersionFallbackJson = "{}"; +String availableVersionFallbackJson = "{}"; +String availableVersionRowsHtml = ""; +Map availableVersionMap = new LinkedHashMap(); +addAvailableVersionInfo(availableVersionMap, "kern", kernXmlPath, kernVersionFilePath); +if (modulePath != null) { + File moduleDir = new File(modulePath); + File[] moduleDirs = moduleDir.exists() && moduleDir.isDirectory() ? moduleDir.listFiles() : null; + if (moduleDirs != null) { + for (int i = 0; i < moduleDirs.length; i++) { + File md = moduleDirs[i]; + if (md != null && md.isDirectory()) { + String abbr = md.getName(); + if (!"kern".equalsIgnoreCase(abbr)) { + addAvailableVersionInfo(availableVersionMap, abbr, joinPath(md.getPath(), "conf/" + abbr + ".xml"), joinPath(md.getPath(), "VERSION")); + } + } + } + } +} +availableVersionFallbackJson = availableVersionJson(availableVersionMap); +availableVersionRowsHtml = availableVersionRows(availableVersionMap); +boolean dbVersionExists = false; +Connection diagCon = null; +try { + diagCon = openDiagnosticConnection(application); + dbDirectAvailable = true; + Statement st = null; + ResultSet rs = null; + try { + st = diagCon.createStatement(); + rs = st.executeQuery("select current_database(), current_schema(), version()"); + if (rs.next()) { + dbInfoHtml = "Datenbank" + html(rs.getString(1)) + "" + + "Schema" + html(rs.getString(2)) + "" + + "DB-Version" + html(rs.getString(3)) + ""; + } + } catch (Exception e) { + dbInfoHtml = "Direkte DB-Information" + tag("nicht verfügbar", "warn") + " " + html(e.getMessage()) + ""; + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (st != null) st.close(); } catch (Exception e) { } + } + PreparedStatement ps = null; + rs = null; + try { + ps = diagCon.prepareStatement("select to_regclass('public.job_log') is not null"); + rs = ps.executeQuery(); + if (rs.next()) jobLogExists = rs.getBoolean(1); + jobLogTableTag = jobLogExists ? tag("vorhanden", "ok") : tag("fehlt", "warn"); + } catch (Exception e) { + jobLogTableTag = tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage()); + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } + if (jobLogExists) { + jobLogColumnsHtml = sqlColumnRows(diagCon, "public", "job_log"); + ps = null; + rs = null; + try { + ps = diagCon.prepareStatement("select column_name from information_schema.columns where table_schema = 'public' and table_name = 'job_log'"); + rs = ps.executeQuery(); + while (rs.next()) { + String c = rs.getString(1); + if ("job_instance_id".equalsIgnoreCase(c)) jobLogHasJobInstanceId = true; + if ("log".equalsIgnoreCase(c)) jobLogHasLog = true; + if ("status".equalsIgnoreCase(c)) jobLogHasStatus = true; + } + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } + } + + ps = null; + rs = null; + try { + ps = diagCon.prepareStatement("select (to_regclass('db_version') is not null or to_regclass('public.db_version') is not null)"); + rs = ps.executeQuery(); + if (rs.next()) dbVersionExists = rs.getBoolean(1); + dbVersionTableTag = dbVersionExists ? tag("vorhanden", "ok") : tag("fehlt", "warn"); + } catch (Exception e) { + dbVersionTableTag = tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage()); + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } + if (dbVersionExists) { + ps = null; + rs = null; + StringBuilder versionJson = new StringBuilder(); + boolean firstVersion = true; + versionJson.append("{"); + try { + ps = diagCon.prepareStatement("select trim(his_system) as his_system, trim(version) as version, systeminfo_id from db_version order by his_system, systeminfo_id"); + rs = ps.executeQuery(); + while (rs.next()) { + String sys = rs.getString(1); + String ver = rs.getString(2); + Integer sid = null; + try { sid = (Integer) rs.getObject(3); } catch (Exception e) { sid = null; } + if (sys != null && ver != null && sys.trim().length() > 0 && ver.trim().length() > 0) { + String key = sys.trim().toLowerCase(java.util.Locale.ROOT); + if (!firstVersion) versionJson.append(","); + versionJson.append("\"").append(jsString(key)).append("\":\"").append(jsString(ver.trim())).append("\""); + firstVersion = false; + if (sid != null) { + versionJson.append(",\"sid:").append(sid.intValue()).append("\":\"").append(jsString(ver.trim())).append("\""); + } + dbVersionRowsHtml += "" + html(sys.trim()) + "" + html(ver.trim()) + "" + html(sid == null ? "" : sid) + ""; + } + } + } catch (Exception e) { + dbVersionRowsHtml = "" + tag("Prüfung fehlgeschlagen", "warn") + " " + html(e.getMessage()) + ""; + } finally { + try { if (rs != null) rs.close(); } catch (Exception e) { } + try { if (ps != null) ps.close(); } catch (Exception e) { } + } + versionJson.append("}"); + dbVersionFallbackJson = versionJson.toString(); + } +} catch (Exception e) { + dbDiagnosticMessage = e.getMessage(); + dbInfoHtml = "Direkte DB-Prüfung" + tag("nicht verfügbar", "warn") + " " + html(e.getMessage()) + ""; +} finally { + try { if (diagCon != null) diagCon.close(); } catch (Exception e) { } +} +%> + + + + + +SuperX ETL Manager + + + + +
+

SuperX ETL Manager

Aufgeräumte Modulübersicht für den täglichen Betrieb. Technische Details, Jobauswahl und JSON-Rohdaten sind über den Detailbereich erreichbar.

+ +
+ + + +
ModulStatusVersionenLetztes UpdateDatenquelleDB-VerbindungAktionen
Module werden geladen...
+ +

Aktueller Lauf

JobExecutionId
-
Job
-
Status
Kein aktiver Lauf bekannt.
Fortschritt
-
+

Log-Ausgabe

Noch kein Log geladen.
Noch kein Lauf gestartet.
+
+ + + + diff --git a/superx/xml/nd_templates.xsl b/superx/xml/nd_templates.xsl index 3b718e2..7871ade 100644 --- a/superx/xml/nd_templates.xsl +++ b/superx/xml/nd_templates.xsl @@ -473,6 +473,7 @@ $( "#content" ).load('/superx/xml/welcome_wiki.jsp',ajaxErrorHandler);