Initial commit.
This commit is contained in:
parent
8385efb9b0
commit
31f9272409
31 changed files with 1795 additions and 0 deletions
42
.classpath
Normal file
42
.classpath
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="module" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
|
</classpath>
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.log
|
||||||
|
/target/
|
||||||
37
.project
Normal file
37
.project
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>com.stephenschafer.budget.web</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.wst.common.project.facet.core.builder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.wst.validation.validationbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||||
|
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
|
||||||
|
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
|
||||||
|
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
12
.settings/.jsdtscope
Normal file
12
.settings/.jsdtscope
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry excluding="**/node_modules/*|**/*.min.js|**/bower_components/*" kind="src" path="src/main/webapp"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.WebProject">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="hide" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
|
||||||
|
<classpathentry kind="output" path=""/>
|
||||||
|
</classpath>
|
||||||
11
.settings/org.eclipse.jdt.core.prefs
Normal file
11
.settings/org.eclipse.jdt.core.prefs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
|
||||||
|
org.eclipse.jdt.core.compiler.compliance=17
|
||||||
|
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||||
|
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||||
|
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||||
|
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||||
|
org.eclipse.jdt.core.compiler.release=enabled
|
||||||
|
org.eclipse.jdt.core.compiler.source=17
|
||||||
4
.settings/org.eclipse.m2e.core.prefs
Normal file
4
.settings/org.eclipse.m2e.core.prefs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
activeProfiles=
|
||||||
|
eclipse.preferences.version=1
|
||||||
|
resolveWorkspaceProjects=true
|
||||||
|
version=1
|
||||||
13
.settings/org.eclipse.wst.common.component
Normal file
13
.settings/org.eclipse.wst.common.component
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><project-modules id="moduleCoreId" project-version="1.5.0">
|
||||||
|
<wb-module deploy-name="budget.web-0.0.1-SNAPSHOT">
|
||||||
|
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
|
||||||
|
<wb-resource deploy-path="/" source-path="/src/main/webapp"/>
|
||||||
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/java"/>
|
||||||
|
<wb-resource deploy-path="/" source-path="/WebContent" tag="defaultRootSource"/>
|
||||||
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
|
||||||
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/java"/>
|
||||||
|
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/test/resources"/>
|
||||||
|
<property name="java-output-path" value="/com.stephenschafer.budget.web/build/classes"/>
|
||||||
|
<property name="context-root" value="budget.web"/>
|
||||||
|
</wb-module>
|
||||||
|
</project-modules>
|
||||||
9
.settings/org.eclipse.wst.common.project.facet.core.xml
Normal file
9
.settings/org.eclipse.wst.common.project.facet.core.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<faceted-project>
|
||||||
|
<fixed facet="jst.web"/>
|
||||||
|
<fixed facet="java"/>
|
||||||
|
<fixed facet="wst.jsdt.web"/>
|
||||||
|
<installed facet="wst.jsdt.web" version="1.0"/>
|
||||||
|
<installed facet="jst.web" version="4.0"/>
|
||||||
|
<installed facet="java" version="17"/>
|
||||||
|
</faceted-project>
|
||||||
1
.settings/org.eclipse.wst.jsdt.ui.superType.container
Normal file
1
.settings/org.eclipse.wst.jsdt.ui.superType.container
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
org.eclipse.wst.jsdt.launching.baseBrowserLibrary
|
||||||
1
.settings/org.eclipse.wst.jsdt.ui.superType.name
Normal file
1
.settings/org.eclipse.wst.jsdt.ui.superType.name
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Window
|
||||||
2
.settings/org.eclipse.wst.validation.prefs
Normal file
2
.settings/org.eclipse.wst.validation.prefs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
disabled=06target
|
||||||
|
eclipse.preferences.version=1
|
||||||
23
WebContent/WEB-INF/web.xml
Normal file
23
WebContent/WEB-INF/web.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" version="2.4"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd http://xmlns.jcp.org/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
|
||||||
|
xmlns:web="http://xmlns.jcp.org/xml/ns/javaee">
|
||||||
|
|
||||||
|
<session-config>
|
||||||
|
<session-timeout>1440</session-timeout>
|
||||||
|
</session-config>
|
||||||
|
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>CategoriesPage</servlet-name>
|
||||||
|
<servlet-class>com.stephenschafer.budget.web.CategoriesPage</servlet-class>
|
||||||
|
<load-on-startup>1</load-on-startup>
|
||||||
|
</servlet>
|
||||||
|
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>CategoriesPage</servlet-name>
|
||||||
|
<url-pattern>/categories</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
|
</web-app>
|
||||||
8
build-war
Executable file
8
build-war
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/sh
|
||||||
|
if mvn -f pom.xml clean install > build-jar.log 2> build-jar.err.log; then
|
||||||
|
echo "success"
|
||||||
|
cp target/*.war /tmp
|
||||||
|
else
|
||||||
|
echo "failure"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
48
pom.xml
Normal file
48
pom.xml
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.stephenschafer</groupId>
|
||||||
|
<artifactId>budget.web</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<packaging>war</packaging>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-war-plugin</artifactId>
|
||||||
|
<version>3.4.0</version>
|
||||||
|
<configuration>
|
||||||
|
<warSourceDirectory>WebContent</warSourceDirectory>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.servlet</groupId>
|
||||||
|
<artifactId>javax.servlet-api</artifactId>
|
||||||
|
<version>4.0.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<version>5.1.38</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
<version>2.18.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.18.1</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
320
src/main/java/com/stephenschafer/budget/web/CategoriesPage.java
Normal file
320
src/main/java/com/stephenschafer/budget/web/CategoriesPage.java
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.servlet.ServletConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class CategoriesPage extends HttpServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(final ServletConfig config) throws ServletException {
|
||||||
|
try {
|
||||||
|
Configuration.INSTANCE.load();
|
||||||
|
}
|
||||||
|
catch (final Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
super.init(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
var paramYear = request.getParameterValues("year");
|
||||||
|
if (paramYear == null || paramYear.length == 0) {
|
||||||
|
final Calendar cal = new GregorianCalendar();
|
||||||
|
cal.setTime(new Date());
|
||||||
|
final var year = cal.get(Calendar.YEAR);
|
||||||
|
paramYear = new String[] { String.valueOf(year) };
|
||||||
|
}
|
||||||
|
var tmpIncludeExpenses = true;
|
||||||
|
var tmpIncludeIncome = true;
|
||||||
|
var tmpIncludePayments = false;
|
||||||
|
var tmpIncludeInvestments = false;
|
||||||
|
final var includes = request.getParameterValues("include");
|
||||||
|
if (includes != null && includes.length > 0) {
|
||||||
|
tmpIncludeExpenses = false;
|
||||||
|
tmpIncludeIncome = false;
|
||||||
|
tmpIncludePayments = false;
|
||||||
|
tmpIncludeInvestments = false;
|
||||||
|
for (final String include : includes) {
|
||||||
|
if ("expenses".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeExpenses = true;
|
||||||
|
}
|
||||||
|
else if ("income".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeIncome = true;
|
||||||
|
}
|
||||||
|
else if ("payments".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludePayments = true;
|
||||||
|
}
|
||||||
|
else if ("investments".equalsIgnoreCase(include)) {
|
||||||
|
tmpIncludeInvestments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var includeExpenses = tmpIncludeExpenses;
|
||||||
|
final var includeIncome = tmpIncludeIncome;
|
||||||
|
final var includePayments = tmpIncludePayments;
|
||||||
|
final var includeInvestments = tmpIncludeInvestments;
|
||||||
|
final var startDates = request.getParameterValues("startDate");
|
||||||
|
final var endDates = request.getParameterValues("endDate");
|
||||||
|
final var startDateString = startDates == null ? null
|
||||||
|
: startDates.length == 0 ? null : startDates[0];
|
||||||
|
final var endDateString = endDates == null ? null
|
||||||
|
: endDates.length == 0 ? null : endDates[0];
|
||||||
|
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
final Date startDate;
|
||||||
|
final Date endDate;
|
||||||
|
try {
|
||||||
|
startDate = startDateString == null ? null : df.parse(startDateString);
|
||||||
|
endDate = endDateString == null ? null : df.parse(endDateString);
|
||||||
|
}
|
||||||
|
catch (final ParseException e) {
|
||||||
|
response.sendError(400, "Invalid date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var out = response.getWriter();
|
||||||
|
response.setHeader("Content-Type", "text/html");
|
||||||
|
try (var connection = Configuration.INSTANCE.getConnection()) {
|
||||||
|
out.println("<html>");
|
||||||
|
out.println("<head>");
|
||||||
|
final var js = Util.getResourceAsString("categories.js");
|
||||||
|
out.println("<script>");
|
||||||
|
out.println(js);
|
||||||
|
out.println("</script>");
|
||||||
|
final var css = Util.getResourceAsString("categories.css");
|
||||||
|
out.println("<style>");
|
||||||
|
out.println(css);
|
||||||
|
out.println("</style>");
|
||||||
|
out.println("</head>");
|
||||||
|
out.println("<body>");
|
||||||
|
for (final String year : paramYear) {
|
||||||
|
final var categoryMap = loadCategories(connection, year, includeIncome,
|
||||||
|
includePayments, includeInvestments, includeExpenses);
|
||||||
|
final var sql = Util.getResourceAsString("getTransactionDetail.sql") //
|
||||||
|
.replace("${databaseName}", "budget_" + year) //
|
||||||
|
.replace("${regexDatabaseName}", "budget");
|
||||||
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
|
try (var rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
final var detail = new Detail();
|
||||||
|
var i = 0;
|
||||||
|
detail.transactionId = rs.getInt(++i);
|
||||||
|
detail.date = rs.getDate(++i);
|
||||||
|
detail.source = rs.getString(++i);
|
||||||
|
detail.description = rs.getString(++i);
|
||||||
|
detail.amount = rs.getBigDecimal(++i);
|
||||||
|
final var categoryId = rs.getInt(++i);
|
||||||
|
detail.regex = rs.getString(++i);
|
||||||
|
detail.flags = rs.getInt(++i);
|
||||||
|
detail.requiredSource = rs.getString(++i);
|
||||||
|
detail.extraDescription = rs.getString(++i);
|
||||||
|
final var category = categoryMap.get(categoryId);
|
||||||
|
if (category != null && //
|
||||||
|
detail.isAfter(startDate) && //
|
||||||
|
detail.isBefore(endDate) && //
|
||||||
|
category.isIncluded()) {
|
||||||
|
category.addDetail(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final var rootCategory = new Category(-1, null, "root");
|
||||||
|
for (final Integer categoryId : categoryMap.keySet()) {
|
||||||
|
final var category = categoryMap.get(categoryId);
|
||||||
|
if ((category != null) && (category.getParent() == null)) {
|
||||||
|
rootCategory.addChild(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.println("<div class=\"tableFixHead\">");
|
||||||
|
out.println("<h1>" + year + " Categories</h1>");
|
||||||
|
out.println("<table>");
|
||||||
|
out.println("<thead>");
|
||||||
|
final var grandHead = Util.getResourceAsString("grandHead.html");
|
||||||
|
out.println(grandHead);
|
||||||
|
out.println("</thead>");
|
||||||
|
out.println("<tbody>");
|
||||||
|
generateCategoryTable(out, rootCategory, 0);
|
||||||
|
out.println("</tbody>");
|
||||||
|
out.println("</table>");
|
||||||
|
out.println("</div>");
|
||||||
|
}
|
||||||
|
out.println("</body>");
|
||||||
|
out.println("</html>");
|
||||||
|
}
|
||||||
|
catch (final SQLException | NamingException e) {
|
||||||
|
throw new ServletException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateCategoryTable(final PrintWriter out, final Category parent,
|
||||||
|
final int level) throws IOException {
|
||||||
|
final var details = parent.getDetails();
|
||||||
|
final var categoryAmount = parent.getDetailTotal();
|
||||||
|
final var categoryGrandAmount = parent.getGrandTotal();
|
||||||
|
var categoryHeadHtml = Util.getResourceAsString("categoryHead.html") //
|
||||||
|
.replace("${name}", parent.getName()) //
|
||||||
|
.replace("${id}", parent.getId().toString()) //
|
||||||
|
.replace("${amount}", categoryAmount.toString()) //
|
||||||
|
.replace("${total}", categoryGrandAmount.toString()) //
|
||||||
|
.replace("${indent}", String.valueOf(level * 20) + "px");
|
||||||
|
final int largestMonth = parent.getLargestMonth();
|
||||||
|
for (int month = 0; month < 12; month++) {
|
||||||
|
categoryHeadHtml = categoryHeadHtml //
|
||||||
|
.replace("${total-" + month + "}", parent.getMonthGrandTotal(month).toString()) //
|
||||||
|
.replace("${color-" + month + "}", month == largestMonth ? "color: red; " : "");
|
||||||
|
}
|
||||||
|
out.print(categoryHeadHtml);
|
||||||
|
final var categoryDetailIntroHtml = Util.getResourceAsString("categoryDetailIntro.html") //
|
||||||
|
.replace("${id}", parent.getId().toString()) //
|
||||||
|
.replace("${indent}", String.valueOf(level * 10) + "px");
|
||||||
|
out.print(categoryDetailIntroHtml);
|
||||||
|
if (!details.isEmpty()) {
|
||||||
|
final var categoryDetailHeadHtml = Util.getResourceAsString("categoryDetailHead.html");
|
||||||
|
out.print(categoryDetailHeadHtml);
|
||||||
|
}
|
||||||
|
Collections.sort(details);
|
||||||
|
for (final Detail detail : details) {
|
||||||
|
final var categoryDetailHtml = Util.getResourceAsString("categoryDetail.html") //
|
||||||
|
.replace("${source}", detail.source) //
|
||||||
|
.replace("${description}", detail.description) //
|
||||||
|
.replace("${date}", detail.date.toString()) //
|
||||||
|
.replace("${amount}", detail.amount.toString()) //
|
||||||
|
.replace("${extraDescription}",
|
||||||
|
detail.extraDescription == null ? "" : detail.extraDescription) //
|
||||||
|
.replace("${regex}", detail.regex) //
|
||||||
|
.replace("${flags}", getFlagsString(detail.flags)) //
|
||||||
|
.replace("${requiredSource}",
|
||||||
|
detail.requiredSource == null ? "" : detail.requiredSource)//
|
||||||
|
;
|
||||||
|
out.print(categoryDetailHtml);
|
||||||
|
}
|
||||||
|
out.println("</table>");
|
||||||
|
out.println("</td>");
|
||||||
|
out.println("</tr>");
|
||||||
|
final var categories = parent.getChildCategories();
|
||||||
|
for (final Category category : categories) {
|
||||||
|
generateCategoryTable(out, category, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFlagsString(final int flags) {
|
||||||
|
final List<String> flagStrings = new ArrayList<>();
|
||||||
|
if ((flags & Pattern.CASE_INSENSITIVE) != 0) {
|
||||||
|
flagStrings.add("Case Insensitive");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.MULTILINE) != 0) {
|
||||||
|
flagStrings.add("Multiline");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.DOTALL) != 0) {
|
||||||
|
flagStrings.add("Dotall");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.UNICODE_CASE) != 0) {
|
||||||
|
flagStrings.add("Unicode Case");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.CANON_EQ) != 0) {
|
||||||
|
flagStrings.add("Canon EQ");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.UNIX_LINES) != 0) {
|
||||||
|
flagStrings.add("Unix Linex");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.LITERAL) != 0) {
|
||||||
|
flagStrings.add("Literal");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.UNICODE_CHARACTER_CLASS) != 0) {
|
||||||
|
flagStrings.add("Unicode Character Class");
|
||||||
|
}
|
||||||
|
if ((flags & Pattern.COMMENTS) != 0) {
|
||||||
|
flagStrings.add("Comments");
|
||||||
|
}
|
||||||
|
return flagStrings.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryFilter {
|
||||||
|
boolean include(Category category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, Category> loadCategories(final Connection connection, final String year,
|
||||||
|
final boolean includeIncome, final boolean includePayments,
|
||||||
|
final boolean includeInvestments, final boolean includeExpenses)
|
||||||
|
throws SQLException, IOException {
|
||||||
|
Category unknownCategory = null;
|
||||||
|
final Map<Integer, Category> categories = new HashMap<>();
|
||||||
|
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
|
||||||
|
"budget");
|
||||||
|
try (var stmt = connection.prepareStatement(sql)) {
|
||||||
|
try (var rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
final Integer id = rs.getInt(1);
|
||||||
|
Integer parentId = rs.getInt(2);
|
||||||
|
if (rs.wasNull()) {
|
||||||
|
parentId = null;
|
||||||
|
}
|
||||||
|
final var name = rs.getString(3);
|
||||||
|
final var category = new Category(id, parentId, name);
|
||||||
|
categories.put(id, category);
|
||||||
|
if ("unknown".equals(name) && parentId == null) {
|
||||||
|
unknownCategory = category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final Integer categoryId : categories.keySet()) {
|
||||||
|
final var category = categories.get(categoryId);
|
||||||
|
category.updateParent(categories);
|
||||||
|
}
|
||||||
|
if (unknownCategory == null) {
|
||||||
|
unknownCategory = new Category(-1, null, "unknown");
|
||||||
|
categories.put(unknownCategory.getId(), unknownCategory);
|
||||||
|
}
|
||||||
|
for (final Integer categoryId : categories.keySet()) {
|
||||||
|
final var category = categories.get(categoryId);
|
||||||
|
final CategoryFilter filter = category1 -> {
|
||||||
|
final String categoryName = category1.getName();
|
||||||
|
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
|
||||||
|
if (!includeIncome) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ("payment".equals(categoryName)) {
|
||||||
|
if (!includePayments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ("investment".equals(categoryName)) {
|
||||||
|
if (!includeInvestments) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!includeExpenses) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
final boolean included = filter.include(category);
|
||||||
|
category.setIncluded(included);
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/main/java/com/stephenschafer/budget/web/Category.java
Normal file
169
src/main/java/com/stephenschafer/budget/web/Category.java
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class Category {
|
||||||
|
private final Integer id;
|
||||||
|
private final Integer parentId;
|
||||||
|
private final String name;
|
||||||
|
private Category parent;
|
||||||
|
private boolean included;
|
||||||
|
private List<Detail> details = new ArrayList<>();
|
||||||
|
private final Map<Integer, Category> children = new HashMap<>();
|
||||||
|
|
||||||
|
public Category(final Integer id, final Integer parentId, final String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.parentId = parentId;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateParent(final Map<Integer, Category> categories) {
|
||||||
|
if (parentId == null) {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parent = categories.get(parentId);
|
||||||
|
parent.addChild(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChild(final Category category) {
|
||||||
|
children.put(category.id, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParent(final Category parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getParentId() {
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQualifiedName() {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
if (parent != null) {
|
||||||
|
sb.append(parent.getQualifiedName());
|
||||||
|
sb.append(".");
|
||||||
|
}
|
||||||
|
sb.append(name);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Detail> getDetails() {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDetails(final List<Detail> details) {
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDetail(final Detail detail) {
|
||||||
|
details.add(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, Category> getChildren() {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getDetailTotal() {
|
||||||
|
var amount = new BigDecimal(0);
|
||||||
|
for (final Detail detail : details) {
|
||||||
|
amount = amount.add(detail.amount);
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMonthTotal(final int month) {
|
||||||
|
var amount = new BigDecimal(0);
|
||||||
|
for (final Detail detail : details) {
|
||||||
|
final Calendar cal = new GregorianCalendar();
|
||||||
|
cal.setTime(detail.date);
|
||||||
|
if (month == cal.get(Calendar.MONTH)) {
|
||||||
|
amount = amount.add(detail.amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getGrandTotal() {
|
||||||
|
var total = getDetailTotal();
|
||||||
|
for (final Integer categoryId : children.keySet()) {
|
||||||
|
final var category = children.get(categoryId);
|
||||||
|
total = total.add(category.getGrandTotal());
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getMonthGrandTotal(final int month) {
|
||||||
|
var total = getMonthTotal(month);
|
||||||
|
for (final Integer categoryId : children.keySet()) {
|
||||||
|
final var category = children.get(categoryId);
|
||||||
|
total = total.add(category.getMonthGrandTotal(month));
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLargestMonth() {
|
||||||
|
BigDecimal max = new BigDecimal(0);
|
||||||
|
int maxMonth = -1;
|
||||||
|
int maxCount = 0;
|
||||||
|
for (int month = 0; month < 12; month++) {
|
||||||
|
final BigDecimal monthAmount = getMonthGrandTotal(month);
|
||||||
|
if (monthAmount.compareTo(max) == 0) {
|
||||||
|
maxCount++;
|
||||||
|
}
|
||||||
|
else if (monthAmount.compareTo(max) > 0) {
|
||||||
|
max = monthAmount;
|
||||||
|
maxMonth = month;
|
||||||
|
maxCount = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxCount > 1 ? -1 : maxMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Category> getChildCategories() {
|
||||||
|
final List<Category> categories = new ArrayList<>();
|
||||||
|
for (final Integer categoryId : children.keySet()) {
|
||||||
|
final var category = children.get(categoryId);
|
||||||
|
categories.add(category);
|
||||||
|
}
|
||||||
|
categories.sort((arg0, arg1) -> {
|
||||||
|
final var name0 = arg0.getName();
|
||||||
|
final var name1 = arg1.getName();
|
||||||
|
final var comparison = name0.compareTo(name1);
|
||||||
|
if (comparison != 0) {
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
final var id0 = arg0.getId();
|
||||||
|
final var id1 = arg1.getId();
|
||||||
|
return id0.compareTo(id1);
|
||||||
|
});
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIncluded() {
|
||||||
|
return included;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIncluded(final boolean include) {
|
||||||
|
this.included = include;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/main/java/com/stephenschafer/budget/web/Configuration.java
Normal file
120
src/main/java/com/stephenschafer/budget/web/Configuration.java
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import javax.naming.InitialContext;
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
public class Configuration {
|
||||||
|
public static final Configuration INSTANCE;
|
||||||
|
private String jndiName;
|
||||||
|
private DbConnectionPool pool;
|
||||||
|
private String privilegedHost;
|
||||||
|
private String defaultDomain;
|
||||||
|
private boolean loaded;
|
||||||
|
|
||||||
|
private Configuration() {
|
||||||
|
jndiName = null;
|
||||||
|
pool = null;
|
||||||
|
loaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load() throws IOException, ClassNotFoundException, SQLException {
|
||||||
|
if (loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final var systemProps = System.getProperties();
|
||||||
|
final var catalinaHome = systemProps.getProperty("catalina.home");
|
||||||
|
var propertiesFileName = System.getenv("BUDGET_PROPERTIES");
|
||||||
|
if (propertiesFileName == null) {
|
||||||
|
propertiesFileName = catalinaHome + "/conf/Catalina/localhost/budget.properties";
|
||||||
|
}
|
||||||
|
final var propertiesFile = new File(propertiesFileName);
|
||||||
|
final var properties = new Properties();
|
||||||
|
try (var fis = new FileInputStream(propertiesFile)) {
|
||||||
|
properties.load(fis);
|
||||||
|
}
|
||||||
|
Logger.logFilename = properties.getProperty("log.filename");
|
||||||
|
Logger.log("********************************* Starting");
|
||||||
|
jndiName = properties.getProperty("db.jndi");
|
||||||
|
final var url = properties.getProperty("db.url");
|
||||||
|
final var username = properties.getProperty("db.username");
|
||||||
|
final var password = properties.getProperty("db.password");
|
||||||
|
if (jndiName == null) {
|
||||||
|
pool = new DbConnectionPool("com.mysql.jdbc.Driver", url, username, password);
|
||||||
|
}
|
||||||
|
privilegedHost = properties.getProperty("priv-host");
|
||||||
|
final var defaultDomain = properties.getProperty("default-domain");
|
||||||
|
this.defaultDomain = defaultDomain == null ? "schafer.cc" : defaultDomain;
|
||||||
|
Logger.log("initialized, jndiName = " + jndiName);
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() throws SQLException, NamingException {
|
||||||
|
final var jndiName = getJndiName();
|
||||||
|
if (jndiName != null) {
|
||||||
|
final var initialContext = new InitialContext();
|
||||||
|
final var datasource = (DataSource) initialContext.lookup(jndiName);
|
||||||
|
SQLException lastException = null;
|
||||||
|
var i = 0;
|
||||||
|
while (i < 4) {
|
||||||
|
try {
|
||||||
|
return datasource.getConnection();
|
||||||
|
}
|
||||||
|
catch (final SQLException e) {
|
||||||
|
Logger.log("Failed to get connection, " + (3 - i) + " retrys left", e);
|
||||||
|
lastException = e;
|
||||||
|
try {
|
||||||
|
Thread.sleep(5000L);
|
||||||
|
}
|
||||||
|
catch (final InterruptedException ex) {
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastException != null) {
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getPool().getConnection(8, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJndiName() {
|
||||||
|
return jndiName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJndiName(final String jndiName) {
|
||||||
|
this.jndiName = jndiName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbConnectionPool getPool() {
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPool(final DbConnectionPool pool) {
|
||||||
|
this.pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE = new Configuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPrivilegedHost() {
|
||||||
|
return privilegedHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultDomain() {
|
||||||
|
return defaultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultDomain(final String defaultDomain) {
|
||||||
|
this.defaultDomain = defaultDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,690 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.sql.Array;
|
||||||
|
import java.sql.Blob;
|
||||||
|
import java.sql.CallableStatement;
|
||||||
|
import java.sql.Clob;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DatabaseMetaData;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.NClob;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLClientInfoException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.SQLWarning;
|
||||||
|
import java.sql.SQLXML;
|
||||||
|
import java.sql.Savepoint;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.sql.Struct;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class DbConnectionPool implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private static final Logger LOGGER;
|
||||||
|
private final String uRL;
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
private final String driver;
|
||||||
|
private final Queue<PooledConnection> connections;
|
||||||
|
private long openConnectionIndex;
|
||||||
|
private final Map<Long, OpenConnectionInfo> openConnections;
|
||||||
|
private long timeout;
|
||||||
|
static {
|
||||||
|
LOGGER = Logger.getLogger(DbConnectionPool.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbConnectionPool(final String dbDriver, final String dbName, final String dbUsername,
|
||||||
|
final String dbPassword) throws ClassNotFoundException, SQLException {
|
||||||
|
connections = new LinkedList<>();
|
||||||
|
openConnectionIndex = 0L;
|
||||||
|
openConnections = new HashMap<>();
|
||||||
|
timeout = 900000L;
|
||||||
|
driver = dbDriver;
|
||||||
|
uRL = dbName;
|
||||||
|
username = dbUsername;
|
||||||
|
password = dbPassword;
|
||||||
|
validateConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void validateConnection() throws ClassNotFoundException, SQLException {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "Testing connection pool");
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "Instantiating " + driver + "\n");
|
||||||
|
Class.forName(driver);
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting");
|
||||||
|
final Connection connection = DriverManager.getConnection(uRL, username, password);
|
||||||
|
try {
|
||||||
|
final DatabaseMetaData dbmd = connection.getMetaData();
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST,
|
||||||
|
"Connection to " + dbmd.getDatabaseProductName() + " "
|
||||||
|
+ dbmd.getDatabaseProductVersion() + " successful.");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void clear() throws SQLException {
|
||||||
|
synchronized (this) {
|
||||||
|
while (!connections.isEmpty()) {
|
||||||
|
final PooledConnection connection = connections.remove();
|
||||||
|
connection.reallyClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final PooledConnection getConnection(final int transactionIsolation,
|
||||||
|
final boolean readOnly, final boolean autoCommit) throws SQLException {
|
||||||
|
while (true) {
|
||||||
|
PooledConnection oldConnection;
|
||||||
|
final long timeout;
|
||||||
|
synchronized (this) {
|
||||||
|
if (connections.isEmpty()) {
|
||||||
|
oldConnection = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
oldConnection = connections.remove();
|
||||||
|
}
|
||||||
|
timeout = this.timeout;
|
||||||
|
}
|
||||||
|
if (oldConnection == null) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "Connecting");
|
||||||
|
SQLException exception = null;
|
||||||
|
int retryCount = 0;
|
||||||
|
while (retryCount < 10) {
|
||||||
|
Connection newConnection;
|
||||||
|
try {
|
||||||
|
newConnection = DriverManager.getConnection(uRL, username, password);
|
||||||
|
}
|
||||||
|
catch (final SQLException e) {
|
||||||
|
if (!(e.getCause() instanceof SocketException)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
exception = e;
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "Retrying", e);
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000L);
|
||||||
|
}
|
||||||
|
catch (final InterruptedException ex) {
|
||||||
|
}
|
||||||
|
++retryCount;
|
||||||
|
newConnection = null;
|
||||||
|
}
|
||||||
|
if (newConnection != null) {
|
||||||
|
newConnection.setAutoCommit(true);
|
||||||
|
newConnection.setTransactionIsolation(transactionIsolation);
|
||||||
|
newConnection.setReadOnly(readOnly);
|
||||||
|
newConnection.setAutoCommit(autoCommit);
|
||||||
|
final long index = incrementOpenConnectionCount();
|
||||||
|
return new PooledConnection(newConnection, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldConnection != null) {
|
||||||
|
if (oldConnection.isClosed()) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||||
|
"Pooled connection was already closed");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Label_0430: {
|
||||||
|
Label_0418: {
|
||||||
|
if ((timeout != 0L) && (System.currentTimeMillis()
|
||||||
|
- oldConnection.getLastAccess() >= timeout)) {
|
||||||
|
break Label_0418;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
oldConnection.setAutoCommit(true);
|
||||||
|
oldConnection.setTransactionIsolation(transactionIsolation);
|
||||||
|
oldConnection.setReadOnly(readOnly);
|
||||||
|
oldConnection.setAutoCommit(autoCommit);
|
||||||
|
synchronized (this) {
|
||||||
|
final Long key = Long.valueOf(oldConnection.getIndex());
|
||||||
|
final OpenConnectionInfo info = openConnections.get(key);
|
||||||
|
if (info != null) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||||
|
"Overwriting open connection info: " + key + " "
|
||||||
|
+ info);
|
||||||
|
}
|
||||||
|
openConnections.put(key, new OpenConnectionInfo());
|
||||||
|
}
|
||||||
|
return oldConnection;
|
||||||
|
}
|
||||||
|
catch (final Exception e2) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.SEVERE,
|
||||||
|
"Unable to reuse DB connection", e2);
|
||||||
|
break Label_0430;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DbConnectionPool.LOGGER.log(Level.FINEST, "DB connection timed out");
|
||||||
|
try {
|
||||||
|
oldConnection.reallyClose();
|
||||||
|
}
|
||||||
|
catch (final Exception e2) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.SEVERE,
|
||||||
|
"Unable to really close DB connection", e2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long incrementOpenConnectionCount() {
|
||||||
|
synchronized (this) {
|
||||||
|
final long index = openConnectionIndex++;
|
||||||
|
final Long key = Long.valueOf(index);
|
||||||
|
final OpenConnectionInfo info = openConnections.get(key);
|
||||||
|
if (info != null) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||||
|
"Overwriting open connection info: " + key + " " + info);
|
||||||
|
}
|
||||||
|
openConnections.put(key, new OpenConnectionInfo());
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void add(final PooledConnection connection) {
|
||||||
|
synchronized (this) {
|
||||||
|
final OpenConnectionInfo info = openConnections.remove(
|
||||||
|
Long.valueOf(connection.getIndex()));
|
||||||
|
if (info == null) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING,
|
||||||
|
"adding orphaned connection: " + connection.getIndex());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
connections.offer(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final long getOpenConnectionIndex() {
|
||||||
|
synchronized (this) {
|
||||||
|
return openConnectionIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final OpenConnectionInfo getOpenConnectionInfo(final long index) {
|
||||||
|
synchronized (this) {
|
||||||
|
return openConnections.get(Long.valueOf(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int getOpenConnectionCount() {
|
||||||
|
synchronized (this) {
|
||||||
|
return openConnections.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final int getPooledConnectionCount() {
|
||||||
|
synchronized (this) {
|
||||||
|
return connections.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
final StringBuilder buf = new StringBuilder();
|
||||||
|
buf.append("DB ");
|
||||||
|
buf.append(uRL);
|
||||||
|
buf.append(" ");
|
||||||
|
synchronized (this) {
|
||||||
|
buf.append("open: ");
|
||||||
|
buf.append(openConnections.size());
|
||||||
|
buf.append(", pooled: ");
|
||||||
|
buf.append(connections.size());
|
||||||
|
buf.append(", next: ");
|
||||||
|
buf.append(openConnectionIndex);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public final long getTimeout() {
|
||||||
|
synchronized (this) {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final void setTimeout(final long timeout) {
|
||||||
|
synchronized (this) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getDriver() {
|
||||||
|
return driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getURL() {
|
||||||
|
return uRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Map<Long, Long> getOpenConnections() {
|
||||||
|
final Map<Long, Long> map = new HashMap<>();
|
||||||
|
for (final Long index : openConnections.keySet()) {
|
||||||
|
final OpenConnectionInfo info = openConnections.get(index);
|
||||||
|
map.put(index, Long.valueOf(info.timestamp));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class OpenConnectionInfo {
|
||||||
|
public final long timestamp;
|
||||||
|
private final List<StackTraceElement> stackTrace;
|
||||||
|
|
||||||
|
public OpenConnectionInfo() {
|
||||||
|
timestamp = System.currentTimeMillis();
|
||||||
|
final StackTraceElement[] steArray = Thread.currentThread().getStackTrace();
|
||||||
|
final List<StackTraceElement> stackTrace = new ArrayList<>(
|
||||||
|
steArray.length);
|
||||||
|
StackTraceElement[] array;
|
||||||
|
for (int length = (array = steArray).length, i = 0; i < length; ++i) {
|
||||||
|
final StackTraceElement element = array[i];
|
||||||
|
stackTrace.add(element);
|
||||||
|
}
|
||||||
|
this.stackTrace = Collections.unmodifiableList(stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Timestamp: ");
|
||||||
|
sb.append(new Date(timestamp));
|
||||||
|
sb.append("\n");
|
||||||
|
for (final StackTraceElement element : stackTrace) {
|
||||||
|
sb.append(element);
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PooledConnection implements Connection {
|
||||||
|
private final Connection connection;
|
||||||
|
private long lastAccess;
|
||||||
|
private boolean autoCommit;
|
||||||
|
private boolean readOnly;
|
||||||
|
private final long index;
|
||||||
|
|
||||||
|
public PooledConnection(final Connection connection, final long index) {
|
||||||
|
lastAccess = System.currentTimeMillis();
|
||||||
|
autoCommit = false;
|
||||||
|
readOnly = false;
|
||||||
|
this.index = index;
|
||||||
|
this.connection = connection;
|
||||||
|
try {
|
||||||
|
autoCommit = connection.getAutoCommit();
|
||||||
|
}
|
||||||
|
catch (final SQLException e) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get auto commit", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
readOnly = connection.isReadOnly();
|
||||||
|
}
|
||||||
|
catch (final SQLException e) {
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING, "Unable to get read only", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearPool() throws SQLException {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String nativeSQL(final String sql) throws SQLException {
|
||||||
|
return connection.nativeSQL(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return connection.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Class<?>> getTypeMap() throws SQLException {
|
||||||
|
return connection.getTypeMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql) throws SQLException {
|
||||||
|
final PreparedStatement stmt = connection.prepareStatement(sql);
|
||||||
|
if (connection.getAutoCommit() != autoCommit || connection.isReadOnly() != readOnly) {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
String sep = "";
|
||||||
|
if (connection.getAutoCommit() != autoCommit) {
|
||||||
|
sb.append(sep);
|
||||||
|
sep = " and ";
|
||||||
|
sb.append("autoCommit has changed from " + autoCommit);
|
||||||
|
}
|
||||||
|
if (connection.isReadOnly() != readOnly) {
|
||||||
|
sb.append(sep);
|
||||||
|
sep = " and ";
|
||||||
|
sb.append("readOnly has changed from " + readOnly);
|
||||||
|
}
|
||||||
|
sb.append(" in ");
|
||||||
|
sb.append(stmt);
|
||||||
|
DbConnectionPool.LOGGER.log(Level.WARNING, sb.toString());
|
||||||
|
}
|
||||||
|
return stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTransactionIsolation(final int level) throws SQLException {
|
||||||
|
connection.setTransactionIsolation(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCatalog() throws SQLException {
|
||||||
|
return connection.getCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTransactionIsolation() throws SQLException {
|
||||||
|
return connection.getTransactionIsolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseSavepoint(final Savepoint savepoint) throws SQLException {
|
||||||
|
connection.releaseSavepoint(savepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHoldability() throws SQLException {
|
||||||
|
return connection.getHoldability();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallableStatement prepareCall(final String sql, final int resultSetType,
|
||||||
|
final int resultSetConcurrency, final int resultSetHoldability)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.prepareCall(sql, resultSetType, resultSetConcurrency,
|
||||||
|
resultSetHoldability);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getAutoCommit() throws SQLException {
|
||||||
|
return connection.getAutoCommit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Statement createStatement() throws SQLException {
|
||||||
|
return connection.createStatement();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallableStatement prepareCall(final String sql) throws SQLException {
|
||||||
|
return connection.prepareCall(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAutoCommit(final boolean autoCommit) throws SQLException {
|
||||||
|
this.autoCommit = autoCommit;
|
||||||
|
connection.setAutoCommit(autoCommit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.prepareStatement(sql, autoGeneratedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadOnly(final boolean readOnly) throws SQLException {
|
||||||
|
this.readOnly = readOnly;
|
||||||
|
connection.setReadOnly(readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CallableStatement prepareCall(final String sql, final int resultSetType,
|
||||||
|
final int resultSetConcurrency) throws SQLException {
|
||||||
|
return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLWarning getWarnings() throws SQLException {
|
||||||
|
return connection.getWarnings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql, final int resultSetType,
|
||||||
|
final int resultSetConcurrency) throws SQLException {
|
||||||
|
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object obj) {
|
||||||
|
return connection.equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.prepareStatement(sql, columnIndexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isClosed() throws SQLException {
|
||||||
|
return connection.isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql, final int resultSetType,
|
||||||
|
final int resultSetConcurrency, final int resultSetHoldability)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.prepareStatement(sql, resultSetType, resultSetConcurrency,
|
||||||
|
resultSetHoldability);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commit() throws SQLException {
|
||||||
|
connection.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearWarnings() throws SQLException {
|
||||||
|
connection.clearWarnings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCatalog(final String catalog) throws SQLException {
|
||||||
|
connection.setCatalog(catalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
add(this);
|
||||||
|
lastAccess = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reallyClose() throws SQLException {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return connection.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DatabaseMetaData getMetaData() throws SQLException {
|
||||||
|
return connection.getMetaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rollback() throws SQLException {
|
||||||
|
connection.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Savepoint setSavepoint(final String name) throws SQLException {
|
||||||
|
return connection.setSavepoint(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() throws SQLException {
|
||||||
|
return connection.isReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Statement createStatement(final int resultSetType, final int resultSetConcurrency)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.createStatement(resultSetType, resultSetConcurrency);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void rollback(final Savepoint savepoint) throws SQLException {
|
||||||
|
connection.rollback(savepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreparedStatement prepareStatement(final String sql, final String[] columnNames)
|
||||||
|
throws SQLException {
|
||||||
|
return connection.prepareStatement(sql, columnNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Savepoint setSavepoint() throws SQLException {
|
||||||
|
return connection.setSavepoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Statement createStatement(final int resultSetType, final int resultSetConcurrency,
|
||||||
|
final int resultSetHoldability) throws SQLException {
|
||||||
|
return connection.createStatement(resultSetType, resultSetConcurrency,
|
||||||
|
resultSetHoldability);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTypeMap(final Map<String, Class<?>> map) throws SQLException {
|
||||||
|
connection.setTypeMap(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHoldability(final int holdability) throws SQLException {
|
||||||
|
connection.setHoldability(holdability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastAccess() {
|
||||||
|
return lastAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Array createArrayOf(final String arg0, final Object[] arg1) throws SQLException {
|
||||||
|
return connection.createArrayOf(arg0, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Blob createBlob() throws SQLException {
|
||||||
|
return connection.createBlob();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Clob createClob() throws SQLException {
|
||||||
|
return connection.createClob();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NClob createNClob() throws SQLException {
|
||||||
|
return connection.createNClob();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLXML createSQLXML() throws SQLException {
|
||||||
|
return connection.createSQLXML();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Struct createStruct(final String arg0, final Object[] arg1) throws SQLException {
|
||||||
|
return connection.createStruct(arg0, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Properties getClientInfo() throws SQLException {
|
||||||
|
return connection.getClientInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientInfo(final String arg0) throws SQLException {
|
||||||
|
return connection.getClientInfo(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(final int arg0) throws SQLException {
|
||||||
|
return connection.isValid(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientInfo(final Properties arg0) throws SQLClientInfoException {
|
||||||
|
connection.setClientInfo(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientInfo(final String arg0, final String arg1)
|
||||||
|
throws SQLClientInfoException {
|
||||||
|
connection.setClientInfo(arg0, arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isWrapperFor(final Class<?> arg0) throws SQLException {
|
||||||
|
return connection.isWrapperFor(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T unwrap(final Class<T> arg0) throws SQLException {
|
||||||
|
return connection.unwrap(arg0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSchema(final String schema) throws SQLException {
|
||||||
|
connection.setSchema(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSchema() throws SQLException {
|
||||||
|
return connection.getSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void abort(final Executor executor) throws SQLException {
|
||||||
|
connection.abort(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNetworkTimeout(final Executor executor, final int milliseconds)
|
||||||
|
throws SQLException {
|
||||||
|
connection.setNetworkTimeout(executor, milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNetworkTimeout() throws SQLException {
|
||||||
|
return connection.getNetworkTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/main/java/com/stephenschafer/budget/web/Detail.java
Normal file
73
src/main/java/com/stephenschafer/budget/web/Detail.java
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
class Detail implements Comparable<Detail> {
|
||||||
|
int transactionId;
|
||||||
|
String source;
|
||||||
|
String description;
|
||||||
|
Date date;
|
||||||
|
BigDecimal amount;
|
||||||
|
String regex;
|
||||||
|
int flags;
|
||||||
|
String requiredSource;
|
||||||
|
String extraDescription;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(final Detail arg1) {
|
||||||
|
final var arg0 = this;
|
||||||
|
int comparison;
|
||||||
|
if (arg0.date == null) {
|
||||||
|
if (arg1.date != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg1.date == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
comparison = arg0.date.compareTo(arg1.date);
|
||||||
|
if (comparison != 0) {
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arg0.source == null) {
|
||||||
|
if (arg1.source != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg1.source == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
comparison = arg0.source.compareTo(arg1.source);
|
||||||
|
if (comparison != 0) {
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arg0.description == null) {
|
||||||
|
if (arg1.description != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (arg1.description == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
comparison = arg0.description.compareTo(arg1.description);
|
||||||
|
if (comparison != 0) {
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAfter(final Date startDate) {
|
||||||
|
return startDate == null || startDate.getTime() <= this.date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBefore(final Date endDate) {
|
||||||
|
return endDate == null || endDate.getTime() > this.date.getTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/java/com/stephenschafer/budget/web/Logger.java
Normal file
76
src/main/java/com/stephenschafer/budget/web/Logger.java
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class Logger {
|
||||||
|
public static String logFilename;
|
||||||
|
public static boolean debugLogging;
|
||||||
|
|
||||||
|
public static void debugLog(final Object message) {
|
||||||
|
if (Logger.debugLogging) {
|
||||||
|
log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void debugLog(final Object message, final Throwable t) {
|
||||||
|
if (Logger.debugLogging) {
|
||||||
|
log(message, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void log(final Object message) {
|
||||||
|
log(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void log(final Object message, final Throwable t) {
|
||||||
|
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ");
|
||||||
|
try {
|
||||||
|
final String filename = Logger.logFilename;
|
||||||
|
if (filename == null) {
|
||||||
|
final PrintStream out = System.out;
|
||||||
|
out.print(df.format(new Date()));
|
||||||
|
out.print(" ");
|
||||||
|
out.println(message);
|
||||||
|
if (t != null) {
|
||||||
|
t.printStackTrace(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final File file = new File(filename);
|
||||||
|
final FileWriter fw = new FileWriter(file, true);
|
||||||
|
final PrintWriter pw = new PrintWriter(fw);
|
||||||
|
try {
|
||||||
|
pw.print(df.format(new Date()));
|
||||||
|
pw.print(" ");
|
||||||
|
pw.println(message);
|
||||||
|
if (t != null) {
|
||||||
|
t.printStackTrace(pw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
pw.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (final IOException e) {
|
||||||
|
System.out.println("Logger: Unable to log " + message);
|
||||||
|
if (t != null) {
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
log("Log failure caused by:");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
Logger.logFilename = null;
|
||||||
|
Logger.debugLogging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/java/com/stephenschafer/budget/web/Util.java
Normal file
22
src/main/java/com/stephenschafer/budget/web/Util.java
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.stephenschafer.budget.web;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
|
||||||
|
public class Util {
|
||||||
|
public static String getResourceAsString(final String resourceName) throws IOException {
|
||||||
|
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
try (final Reader reader = new InputStreamReader(
|
||||||
|
classLoader.getResourceAsStream(resourceName))) {
|
||||||
|
final char[] buffer = new char[0x1000];
|
||||||
|
int charsRead = reader.read(buffer);
|
||||||
|
while (charsRead >= 0) {
|
||||||
|
sb.append(buffer, 0, charsRead);
|
||||||
|
charsRead = reader.read(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main/resources/categories.css
Normal file
6
src/main/resources/categories.css
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.tableFixHead { overflow: auto; height: 100vh; }
|
||||||
|
.tableFixHead thead th { position: sticky; top: 0; z-index: 1; }
|
||||||
|
|
||||||
|
table { border-collapse: collapse; width: 100%; }
|
||||||
|
th, td { padding: 8px 16px; }
|
||||||
|
th { background: #eee; }
|
||||||
11
src/main/resources/categories.js
Normal file
11
src/main/resources/categories.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
function showDetail(h2, categoryId) {
|
||||||
|
var tr = document.body.querySelector("#cat" + categoryId);
|
||||||
|
if(tr.style.display == "none") {
|
||||||
|
tr.style.display = "";
|
||||||
|
h2.style.fontWeight = "bold";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tr.style.display = "none";
|
||||||
|
h2.style.fontWeight = "normal";
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/resources/categoryDetail.html
Normal file
10
src/main/resources/categoryDetail.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<tr>
|
||||||
|
<td>${source}</td>
|
||||||
|
<td style="padding-left: 20px;">${date}</td>
|
||||||
|
<td style="padding-left: 20px;">${description}</td>
|
||||||
|
<td style="padding-left: 20px;">${extraDescription}</td>
|
||||||
|
<td style="padding-left: 20px; text-align: right;">${amount}</td>
|
||||||
|
<td style="padding-left: 20px;">${regex}</td>
|
||||||
|
<td style="padding-left: 20px;">${flags}</td>
|
||||||
|
<td style="padding-left: 20px;">${requiredSource}</td>
|
||||||
|
</tr>
|
||||||
10
src/main/resources/categoryDetailHead.html
Normal file
10
src/main/resources/categoryDetailHead.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<tr>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Extra Description</th>
|
||||||
|
<th style="text-align: right">Amount</th>
|
||||||
|
<th>Regex</th>
|
||||||
|
<th>Flags</th>
|
||||||
|
<th>Required Source</th>
|
||||||
|
</tr>
|
||||||
3
src/main/resources/categoryDetailIntro.html
Normal file
3
src/main/resources/categoryDetailIntro.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<tr id="cat${id}" style="display: none">
|
||||||
|
<td style="padding-left: ${indent}; " colspan="15">
|
||||||
|
<table>
|
||||||
47
src/main/resources/categoryHead.html
Normal file
47
src/main/resources/categoryHead.html
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<tr>
|
||||||
|
<td style="padding-left: ${indent}; ">
|
||||||
|
<h2 style="cursor: pointer; font-weight: normal;" onclick="showDetail(this, ${id})">${name}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ">${amount}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ">${total}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-0}">${total-0}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-1}">${total-1}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-2}">${total-2}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-3}">${total-3}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-4}">${total-4}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-5}">${total-5}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-6}">${total-6}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-7}">${total-7}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-8}">${total-8}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-9}">${total-9}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-10}">${total-10}</h2>
|
||||||
|
</td>
|
||||||
|
<td style="padding-left: 15px; text-align: right; ">
|
||||||
|
<h2 style="font-weight: normal; ${color-11}">${total-11}</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
2
src/main/resources/getCategories.sql
Normal file
2
src/main/resources/getCategories.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
select id, parent_category_id, name
|
||||||
|
from ${databaseName}.category
|
||||||
3
src/main/resources/getTransactionDetail.sql
Normal file
3
src/main/resources/getTransactionDetail.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
select t.id, t.date, t.source, t.description, t.amount, r.category_id, r.regex, r.flags, r.source, r.description
|
||||||
|
from ${databaseName}.transaction t
|
||||||
|
inner join ${regexDatabaseName}.regex r on r.id = t.regex_id
|
||||||
17
src/main/resources/grandHead.html
Normal file
17
src/main/resources/grandHead.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<tr>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Jan</th>
|
||||||
|
<th>Feb</th>
|
||||||
|
<th>Mar</th>
|
||||||
|
<th>Apr</th>
|
||||||
|
<th>May</th>
|
||||||
|
<th>Jun</th>
|
||||||
|
<th>Jul</th>
|
||||||
|
<th>Aug</th>
|
||||||
|
<th>Sep</th>
|
||||||
|
<th>Oct</th>
|
||||||
|
<th>Nov</th>
|
||||||
|
<th>Dec</th>
|
||||||
|
</tr>
|
||||||
3
src/main/webapp/META-INF/MANIFEST.MF
Normal file
3
src/main/webapp/META-INF/MANIFEST.MF
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Manifest-Version: 1.0
|
||||||
|
Class-Path:
|
||||||
|
|
||||||
Loading…
Reference in a new issue