/**
 * Copyright (C) 2015 Anthony K. Trinh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.github.tony19.loggly.ILogglyClient;
import com.github.tony19.loggly.LogglyClient;

import java.util.Arrays;

/**
 * Demonstrates Loggly client usage
 */
public class LogglyClientDemo {

    /**
     * Runs a demo of the Loggly client
     *
     * @param args command-line arguments. The first and only argument
     *             should be your Loggly customer token.
     *             http://loggly.com/docs/customer-token-authentication-token/
     */
    public static void main(String... args) {

        if (args.length == 0 || args[0].trim().isEmpty()) {
            System.err.println("missing argument: loggly token\nsee http://loggly.com/docs/customer-token-authentication-token/");
            System.exit(1);
        }

        final String TOKEN = args[0];
        final ILogglyClient loggly = new LogglyClient(TOKEN);

        System.out.println("posting single event to Loggly asynchronously...");
        loggly.log("Hello!\nThis is a\nmulti-line event!\n",
                new LogglyClient.Callback() {
                    public void success() {
                        System.out.println("callback succeeded");
                    }

                    public void failure(String error) {
                        System.err.println("callback failed: " + error);
                    }
                });

        System.out.println("posting bulk events to Loggly asynchronously...");
        loggly.logBulk(Arrays.asList("E1", "E2"),
                new LogglyClient.Callback() {
                    public void success() {
                        System.out.println("bulk callback succeeded");
                    }

                    public void failure(String error) {
                        System.err.println("bulk callback failed: " + error);
                    }
                });

        System.out.println("posting single event to Loggly...");
        boolean ok = loggly.log("Hello!\nThis is a\nmulti-line event!\n");
        System.out.println(ok ? "ok" : "err");

        System.out.println("posting single JSON event to Loggly...");
        final String json = "{ \"timestamp\": \"2015-01-01T12:34:00Z\", \"message\": \"Event 100\", \"count\": 100 }";
        ok = loggly.log(json);
        System.out.println(ok ? "ok" : "err");

        System.out.println("posting bulk events to Loggly...");
        ok = loggly.logBulk("This is a\nmulti-line event 1", "Event 2", "Event 3");
        System.out.println(ok ? "ok" : "err");

        System.out.println("setting log tags to 'foo', 'bar', and 'baz'...");
        loggly.setTags("foo", "bar,baz");
        ok = loggly.log("This should be tagged with 'foo', 'bar', and 'baz'");
        System.out.println(ok ? "ok" : "err");
    }
}


/*
 * ao-dbc - Simplified JDBC access for simplified code.
 * Copyright (C) 2011, 2013, 2015, 2016  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-dbc.
 *
 * ao-dbc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-dbc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-dbc.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.dbc.meta;

import com.aoindustries.dbc.NoRowException;
import com.aoindustries.util.AoCollections;
import com.aoindustries.util.graph.Edge;
import com.aoindustries.util.graph.SymmetricGraph;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * A friendly wrapper around database meta data.
 *
 * @author  AO Industries, Inc.
 */
public class Catalog {

	private final DatabaseMetaData metaData;
	private final String name;

	protected Catalog(DatabaseMetaData metaData, String name) {
		this.metaData = metaData;
		this.name = name;
	}

	@Override
	public String toString() {
		return name;
	}

	public DatabaseMetaData getMetaData() {
		return metaData;
	}

	public String getName() {
		return name;
	}

	private static class GetSchemasLock {}
	private final GetSchemasLock getSchemasLock = new GetSchemasLock();
	private SortedMap<String,Schema> getSchemasCache;

	/**
	 * Gets all schemas for this catalog keyed by unique name.
	 *
	 * @see  java.sql.DatabaseMetaData#getSchemas()
	 */
	public SortedMap<String,Schema> getSchemas() throws SQLException {
		synchronized(getSchemasLock) {
			if(getSchemasCache==null) {
				SortedMap<String,Schema> newSchemas = new TreeMap<>(DatabaseMetaData.getCollator());
				try (ResultSet results = metaData.getMetaData().getSchemas()) {
					ResultSetMetaData resultsMeta = results.getMetaData();
					while(results.next()) {
						int colCount = resultsMeta.getColumnCount();
						//System.err.println("DEBUG: Catalog: getSchemas(): colCount=" + colCount);
						//for(int i=1; i<=colCount; i++) {
						//	resultsMeta.getColumnName(i);
						//	System.err.println("DEBUG: Catalog: getSchemas(): resultsMeta.getColumnName("+i+")=" + resultsMeta.getColumnName(i));
						//}
						//System.err.println("DEBUG: Catalog: getSchemas(): results.getString(\"table_catalog\")=" + results.getString("table_catalog"));
						//System.err.println("DEBUG: Catalog: getSchemas(): results.getString(\"TABLE_CATALOG\")=" + results.getString("TABLE_CATALOG"));
						if(
							colCount==1 // PostgreSQL 8.3 only returns one column
							|| results.getString("TABLE_CATALOG") == null // PostgreSQL 9.4 driver returns null
							|| name.equals(results.getString("TABLE_CATALOG")) // Other driver
						) {
							Schema newSchema = new Schema(this, results.getString("TABLE_SCHEM"));
							if(newSchemas.put(newSchema.getName(), newSchema)!=null) throw new AssertionError("Duplicate schema: "+newSchema);
						}
					}
				}
				getSchemasCache = AoCollections.optimalUnmodifiableSortedMap(newSchemas);
			}
			return getSchemasCache;
		}
	}

	/**
	 * Gets the schema of the provided name.
	 *
	 * @throws  NoRowException if the schema doesn't exist
	 */
	public Schema getSchema(String name) throws NoRowException, SQLException {
		Schema schema = getSchemas().get(name);
		if(schema==null) throw new NoRowException("name=" + name);
		return schema;
	}

	/**
	 * Gets a graph view of the imported/exported table relationships within this catalog.
	 */
	public SymmetricGraph getForeignKeyGraph() {
		return getForeignKeyGraph(null);
	}

	/**
	 * Gets a graph view of the imported/exported table relationships within this catalog.
	 *
	 * TODO: Check is symmetric in JUnit test
	 *
	 * @param the set of tables types or null for all types
	 */
	public SymmetricGraph getForeignKeyGraph(final Set<String> tableTypes) {
		return new SymmetricGraph() {

			@Override
			public Set
 getVertices() throws SQLException {
				Set
 vertices = new LinkedHashSet<>();
				for(Schema schema : getSchemas().values()) {
					for(Table table : schema.getTables().values()) {
						if(tableTypes==null || tableTypes.contains(table.getTableType())) vertices.add(table);
					}
				}
				return AoCollections.optimalUnmodifiableSet(vertices);
			}

			@Override
			public Set<Edge
> getEdgesFrom(Table from) throws SQLException {
				Set<? extends Table> tos = from.getImportedTables();
				Set<Edge
> edges = new LinkedHashSet<>(tos.size()*4/3+1);
				for(Table to : tos) {
					if(
						tableTypes==null
						|| tableTypes.contains(from.getTableType())
						|| tableTypes.contains(to.getTableType())
					) edges.add(new Edge<>(from, to));
				}
				return AoCollections.optimalUnmodifiableSet(edges);
			}

			@Override
			public Set<Edge
> getEdgesTo(Table to) throws SQLException {
				Set<? extends Table> froms = to.getExportedTables();
				Set<Edge
> edges = new LinkedHashSet<>(froms.size()*4/3+1);
				for(Table from : froms) {
					if(
						tableTypes==null
						|| tableTypes.contains(from.getTableType())
						|| tableTypes.contains(to.getTableType())
					) edges.add(new Edge<>(from, to));
				}
				return AoCollections.optimalUnmodifiableSet(edges);
			}
		};
	}
}


/*
 * semanticcms-autogit-servlet - SemanticCMS automatic Git in a Servlet environment.
 * Copyright (C) 2016, 2018  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of semanticcms-autogit-servlet.
 *
 * semanticcms-autogit-servlet is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * semanticcms-autogit-servlet is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with semanticcms-autogit-servlet.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.semanticcms.autogit.servlet;

import com.aoindustries.lang.ProcessResult;
import com.aoindustries.util.StringUtility;
import com.aoindustries.util.WrappedException;
import com.semanticcms.autogit.model.GitStatus;
import com.semanticcms.autogit.model.State;
import com.semanticcms.autogit.model.UncommittedChange;
import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.WebListener;

@WebListener("Manages automatic Git repository interactions.")
public class AutoGitContextListener implements ServletContextListener {

	private static final boolean DEBUG = false; // false for production releases

	private static final String APPLICATION_SCOPE_KEY = AutoGitContextListener.class.getName();

	/**
	 * The lock file used by Git that ignored for file changes.
	 */
	private static final String GIT_LOCK_FILE = "index.lock";

	/**
	 * The number of milliseconds between automatic "git pull".
	 */
	private static final long GIT_PULL_MILLIS = 5L * 60L * 1000L; // 5 minutes

	/**
	 * The number of milliseconds after a change is detected to begin the Git status update.
	 */
	private static final long AFTER_CHANGE_DELAY = 1000;

	/**
	 * The number of milliseconds to delay after an exception.
	 */
	private static final long AFTER_EXCEPTION_DELAY = 10000;

	/**
	 * The number of milliseconds that status not updated until considered "TIMEOUT"
	 */
	private static final long TIMEOUT_MILLIS = GIT_PULL_MILLIS * 2;

	private static final String GIT_TOPLEVEL_CONTEXT_PARAM = "git.toplevel";

	private static class ServletContextLock {}
	private final ServletContextLock servletContextLock = new ServletContextLock();
	private ServletContext servletContext;

	private static class GitToplevelLock {}
	private final GitToplevelLock gitToplevelLock = new GitToplevelLock();
	private Path gitToplevel;

	private static class WatcherLock {}
	private final WatcherLock watcherLock = new WatcherLock();
	private WatchService watcher; // Will be set to null when context shutdown

	private final Map<Path,WatchKey> registered = new HashMap<>();

	private static class WatcherThreadLock {}
	private final WatcherThreadLock watcherThreadLock = new WatcherThreadLock();
	private Thread watcherThread; // Set to null when context shutdown

	private static class ChangedThreadLock {}
	private final ChangedThreadLock changedThreadLock = new ChangedThreadLock();
	private Thread changedThread; // Set to null when context shutdown

	@Override
	public void contextInitialized(ServletContextEvent sce) {
		try {
			ServletContext sc;
			synchronized(servletContextLock) {
				servletContext = sce.getServletContext();
				sc = servletContext;
			}
			// Find the top level directory
			String gitToplevelPath = sc.getInitParameter(GIT_TOPLEVEL_CONTEXT_PARAM);
			File gitToplevelRaw;
			if(gitToplevelPath == null || gitToplevelPath.isEmpty()) {
				// Default to web root
				String rootRealPath = sc.getRealPath("/");
				if(rootRealPath == null) throw new IllegalStateException("Unable to find web root and " + GIT_TOPLEVEL_CONTEXT_PARAM + " context parameter not provided");
				gitToplevelRaw = new File(rootRealPath);
			} else {
				if(gitToplevelPath.startsWith("~/")) {
					gitToplevelRaw = new File(System.getProperty("user.home"), gitToplevelPath.substring(2));
				} else {
					gitToplevelRaw = new File(gitToplevelPath);
				}
			}
			if(DEBUG) sc.log("gitToplevelRaw: " + gitToplevelRaw);
			Path gtl;
			synchronized(gitToplevelLock) {
				gitToplevel = gitToplevelRaw.getCanonicalFile().toPath();
				gtl = gitToplevel;
			}
			if(DEBUG) sc.log("gitToplevel: " + gtl);
			// Make sure root exists and is readable
			if(!Files.isDirectory(gtl, LinkOption.NOFOLLOW_LINKS)) throw new IOException("Git toplevel is not a directory: " + gtl);
			if(!Files.isReadable(gtl)) throw new IOException("Unable to read Git toplevel directory: " + gtl);
			// Recursively watch for any changes in the directory
			if(DEBUG) sc.log("Starting watcher");
			WatchService w;
			synchronized(watcherLock) {
				watcher = gtl.getFileSystem().newWatchService();
				w = watcher;
			}
			resync();
			if(DEBUG) sc.log("Starting watchThread");
			synchronized(watcherThreadLock) {
				watcherThread = new Thread(watcherRunnable);
				watcherThread.start();
			}
			if(DEBUG) sc.log("Starting changeThread");
			synchronized(changedThreadLock) {
				changedThread = new Thread(changedRunnable);
				changedThread.start();
			}
			sc.setAttribute(APPLICATION_SCOPE_KEY, this);
		} catch(IOException e) {
			throw new WrappedException(e);
		}
	}

	private void log(String message) {
		ServletContext sc;
		synchronized(servletContextLock) {
			sc = servletContext;
		}
		if(sc != null) sc.log(message);
	}

	private void log(String message, Throwable t) {
		ServletContext sc;
		synchronized(servletContextLock) {
			sc = servletContext;
		}
		if(sc != null) sc.log(message, t);
	}

	/**
	 * Resyncs the entire directory recursively, registering and canceling any
	 * discrepancies found.
	 */
	private void resync() throws IOException {
		Path gtl;
		synchronized(gitToplevelLock) {
			gtl = gitToplevel;
		}
		WatchService w;
		synchronized(watcherLock) {
			w = watcher;
		}
		if(gtl != null && w != null) {
			synchronized(registered) {
				Set<Path> extraKeys = new HashSet<>(registered.keySet());
				resync(w, gtl, extraKeys);
				for(Path extraKey : extraKeys) {
					if(DEBUG) log("Canceling watch key: " + extraKey);
					registered.remove(extraKey).cancel();
				}
			}
		}
	}

	private void resync(WatchService w, Path path, Set<Path> extraKeys) throws IOException {
		assert Thread.holdsLock(registered);
		if(
			Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)
			// Skip .git directory "index.lock" files
			&& !path.endsWith(GIT_LOCK_FILE)
			//&& !path.endsWith(".git")
		) {
			WatchKey key = registered.get(path);
			if(key == null) {
				if(DEBUG) log("Registering watch key: " + path);
				registered.put(
					path,
					path.register(
						w,
						StandardWatchEventKinds.ENTRY_CREATE,
						StandardWatchEventKinds.ENTRY_DELETE,
						StandardWatchEventKinds.ENTRY_MODIFY
					)
				);
			} else if(!key.isValid()) {
				if(DEBUG) log("Replacing invalid watch key: " + path);
				registered.put(
					path,
					path.register(
						w,
						StandardWatchEventKinds.ENTRY_CREATE,
						StandardWatchEventKinds.ENTRY_DELETE,
						StandardWatchEventKinds.ENTRY_MODIFY
					)
				);
			}
			extraKeys.remove(path);
			try (DirectoryStream<Path> files = Files.newDirectoryStream(path)) {
				for(Path file : files) {
					resync(w, file, extraKeys);
				}
			}
		}
	}

	/** Flag set to true whenever a possible change is detected */
	private static class ChangedLock {}
	private final ChangedLock changedLock = new ChangedLock();
	private boolean changed = false;

	// Java 1.8: Use Lambda
	private final Runnable watcherRunnable = new Runnable() {
		@Override
		public void run() {
			while(true) {
				try {
					// Stop when thread set to null
					synchronized(watcherThreadLock) {
						if(watcherThread == null) return;
					}
					// Stop when watcher set to null
					WatchService w;
					synchronized(watcherLock) {
						w = watcher;
					}
					if(w == null) return;
					// Get next key
					WatchKey key;
					try {
						key = w.take();
					} catch(ClosedWatchServiceException e) {
						// Normal on shutdown
						return;
					} catch(InterruptedException e) {
						// Repeat loop
						continue;
					}
					boolean doResync = false;
					boolean setChanged = false;
					for(WatchEvent<?> event : key.pollEvents()) {
						WatchEvent.Kind<?> kind = event.kind();
						if(kind == StandardWatchEventKinds.OVERFLOW) {
							if(DEBUG) log("Overflow");
							// Reevaluates all recursively on overflow
							doResync = true;
							setChanged = true;
						} else if(
							kind == StandardWatchEventKinds.ENTRY_CREATE
							|| kind == StandardWatchEventKinds.ENTRY_DELETE
							|| kind == StandardWatchEventKinds.ENTRY_MODIFY
						) {
							@SuppressWarnings("unchecked")
							WatchEvent<Path> ev = (WatchEvent<Path>)event;
							Path filename = ev.context();
							if(filename.endsWith(GIT_LOCK_FILE)) {
								if(DEBUG) log(kind + ": " + filename + " (Skipping Git lock)");
							} else {
								if(DEBUG) log(kind + ": " + filename);
								if(!doResync) {
									// TODO: Add/remove directories more efficiently given specific kind and paths instead of full resync
									doResync = true;
								}
								setChanged = true;
							}
						} else {
							throw new AssertionError("Unexpected kind: " + kind);
						}
					}
					if(doResync) {
						resync();
					}
					if(setChanged) {
						synchronized(changedLock) {
							changed = true;
							changedLock.notify();
						}
					}
					boolean valid = key.reset();
					if(!valid) {
						// Remove from registered?
					}
				} catch(ThreadDeath td) {
					throw td;
				} catch(Throwable t) {
					log(null, t);
					try {
						Thread.sleep(10000);
					} catch(InterruptedException e) {
						// Continue loop
					}
				}
			}
		}
	};

	// Java 1.8: Use Lambda
	private final Runnable changedRunnable = new Runnable() {
		@Override
		public void run() {
			while(true) {
				try {
					// Stop when thread set to null
					synchronized(changedThreadLock) {
						if(changedThread == null) return;
					}
					// Clear changed flag now before updating Git status so any change during status check will trigger
					// another change detection
					synchronized(changedLock) {
						changed = false;
					}
					// Do git now
					updateGitStatus();
					long waitEndTime = System.currentTimeMillis() + GIT_PULL_MILLIS;
					boolean changeReceived;
					synchronized(changedLock) {
						while(true) {
							// Stop when thread set to null
							synchronized(changedThreadLock) {
								if(changedThread == null) return;
							}
							// Check if changed
							if(changed) {
								changeReceived = true;
								break;
							} else {
								// Wait at most up to GIT_PULL_MILLIS until changed flag set
								long now = System.currentTimeMillis();
								long millisToWait = waitEndTime - now;
								if(millisToWait <= 0) {
									if(DEBUG) log("changeRunnable, no more waiting");
									changeReceived = false;
									break;
								}
								if(millisToWait > GIT_PULL_MILLIS) {
									// System time reset into the past
									if(DEBUG) log("changeRunnable, system time reset into the past");
									waitEndTime = now + GIT_PULL_MILLIS;
									millisToWait = GIT_PULL_MILLIS;
								}
								if(DEBUG) log("changeRunnable, waiting up to " + millisToWait + " ms");
								try {
									changedLock.wait(millisToWait);
								} catch(InterruptedException e) {
									// Normal on shutdown
								}
							}
						}
					}
					if(changeReceived) {
						// Delay a bit to batch changes
						if(DEBUG) log("changeRunnable, change received");
						try {
							Thread.sleep(AFTER_CHANGE_DELAY);
						} catch(InterruptedException e) {
							// Continue on
						}
					}
				} catch(ThreadDeath td) {
					throw td;
				} catch(Throwable t) {
					log(null, t);
					try {
						Thread.sleep(AFTER_EXCEPTION_DELAY);
					} catch(InterruptedException e) {
						// Continue loop
					}
				}
			}
		}
	};

	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		ServletContext sc = sce.getServletContext();
		sc.removeAttribute(APPLICATION_SCOPE_KEY);

		Thread ct;
		synchronized(changedThreadLock) {
			ct = changedThread;
			changedThread = null;
		}
		if(ct != null) {
			if(DEBUG) log("Shutting down changeThread");
			ct.interrupt();
		}

		Thread wt;
		synchronized(watcherThreadLock) {
			wt = watcherThread;
			watcherThread = null;
		}
		if(wt != null) {
			if(DEBUG) log("Shutting down watchThread");
			wt.interrupt();
		}

		synchronized(watcherLock) {
			if(watcher != null) {
				if(DEBUG) log("Shutting down watcher");
				try {
					watcher.close();
				} catch(IOException e) {
					throw new WrappedException(e);
				}
				watcher = null;
			}
		}

		synchronized(registered) {
			/*
			for(Map.Entry<Path,WatchKey> entry : registered.entrySet()) {
				if(DEBUG) log("Canceling watch key: " + entry.getKey());
				entry.getValue().cancel();
			}*/
			registered.clear();
		}

		synchronized(gitToplevelLock) {
			gitToplevel = null;
		}

		synchronized(servletContextLock) {
			servletContext = null;
		}
	}

	private static class StatusLock {}
	private final StatusLock statusLock = new StatusLock();
	private GitStatus status;
	{
		// Java 1.8: Inline this
		List<UncommittedChange> emptyList = Collections.emptyList();
		status = new GitStatus(System.currentTimeMillis(), State.STARTING, emptyList);
	}

	/**
	 * Updates the current Git status, will block on I/O.
	 * Automatically does git pull and git push when there are no uncommitted changes.
	 * <p>
	 * Called on startup.
	 * Also called when file change detected and the Git status should be updated.
	 * </p>
	 * TODO: Add timeout within this method?
	 */
	private void updateGitStatus() throws IOException, ParseException {
		long now = System.currentTimeMillis();
		Path gtl;
		synchronized(gitToplevelLock) {
			gtl = gitToplevel;
		}
		GitStatus newStatus;
		if(gtl == null) {
			// Java 1.8: Inline this
			List<UncommittedChange> emptyList = Collections.emptyList();
			newStatus = new GitStatus(now, State.DISABLED, emptyList);
		} else {
			// Get a list of modules including "" for main module
			List<String> modules;
			{
				if(DEBUG) log("Finding modules");
				ProcessBuilder pb =
					new ProcessBuilder("git", "submodule", "--quiet", "foreach", "--recursive", "echo \"$path\"")
					.directory(gtl.toFile())
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to find submodules: " + result.getStderr());
				List<String> submodules = StringUtility.splitLines(result.getStdout());
				if(DEBUG) {
					for(String submodule : submodules) log("Got submodule: " + submodule);
				}
				// Add the empty module list
				modules = new ArrayList<>(submodules.size() + 1);
				modules.addAll(submodules);
				modules.add("");
			}
			// Get the status of each module
			State state = State.SYNCHRONIZED;
			List<UncommittedChange> uncommittedChanges = new ArrayList<>();
			for(String module : modules) {
				if(DEBUG) log("Getting status of module \"" + module + "\"");
				File workingDir;
				if(module.isEmpty()) {
					// Main module
					workingDir = gtl.toFile();
				} else {
					// Submodule
					workingDir = new File(gtl.toFile(), module);
				}
				ProcessBuilder pb =
					new ProcessBuilder("git", "status", "--porcelain", "-z")
					.directory(workingDir)
				;
				Process p = pb.start();
				ProcessResult result = ProcessResult.getProcessResult(p);
				if(result.getExitVal() != 0) throw new IOException("Unable to get status: " + result.getStderr());
				// Split on NUL (ASCII 0)
				List<String> split = new ArrayList<>(StringUtility.splitString(result.getStdout(), (char)0));
				if(!split.isEmpty()) {
					// Remove last empty part of split
					String last = split.remove(split.size()-1);
					if(!last.isEmpty()) throw new ParseException("Last element of split is not empty: " + last, 0);
				}
				//if((split.size() % 2) != 0) throw new ParseException("Unexpected split size: " + split.size(), 0);
				int i = 0;
				while(i < split.size()) {
					char x;
					char y;
					String from;
					String to;
					{
						String first = split.get(i++);
						if(first.length() < 3) throw new ParseException("split1 length too short: " + first.length(), 0);
						x = first.charAt(0);
						y = first.charAt(1);
						if(first.charAt(2) != ' ') throw new ParseException("Third character of split1 is not a space: " + first.charAt(2), 0);
						if(x == 'R') {
							// Is rename, will have both to and from
							to = first.substring(3);
							from = split.get(i++);
						} else {
							// Will have from only from
							to = null;
							from = first.substring(3);
						}
					}
					if(DEBUG) {
						log("x = \"" + x + "\"");
						log("y = \"" + y + "\"");
						log("from = \"" + from + "\"");
						log("to = \"" + to + "\"");
					}
					UncommittedChange uncommittedChange = new UncommittedChange(x, y, module, from, to);
					uncommittedChanges.add(uncommittedChange);
					// Keep the highest state
					State meaningState = uncommittedChange.getMeaning().getState();
					if(meaningState.compareTo(state) > 0) state = meaningState;
				}
			}
			if(DEBUG) log("state = " + state);
			// TODO: auto-commit any modified tasklogs
			// TODO: git pull all modules always to stay in sync
			// TODO: git push all modules if state is synchronized
			newStatus = new GitStatus(now, state, Collections.unmodifiableList(uncommittedChanges));
		}
		synchronized(statusLock) {
			status = newStatus;
		}
	}

	/**
	 * Gets the current Git status, will not block.
	 */
	public GitStatus getGitStatus() {
		long now = System.currentTimeMillis();
		synchronized(statusLock) {
			// Timeout when status not updated recently enough
			long statusTime = status.getStatusTime();
			long millisSince = now - statusTime;
			if(
				millisSince >= TIMEOUT_MILLIS
				|| millisSince <= -TIMEOUT_MILLIS // Can happen when system time reset
			) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				return new GitStatus(statusTime, State.TIMEOUT, emptyList);
			}
			// Return most recently determined status
			return status;
		}
	}

	public static AutoGitContextListener getInstance(ServletContext sc) {
		return (AutoGitContextListener)sc.getAttribute(APPLICATION_SCOPE_KEY);
	}

	private static final String GIT_STATUS_REQUEST_CACHE_KEY = AutoGitContextListener.class.getName() + ".getGitStatus.cache";

	public static GitStatus getGitStatus(
		ServletContext servletContext,
		ServletRequest request
	) {
		// Look for cached value
		GitStatus gitStatus = (GitStatus)request.getAttribute(GIT_STATUS_REQUEST_CACHE_KEY);
		if(gitStatus == null) {
			AutoGitContextListener gitContext = AutoGitContextListener.getInstance(servletContext);
			if(gitContext == null) {
				// Java 1.8: Inline this
				List<UncommittedChange> emptyList = Collections.emptyList();
				gitStatus = new GitStatus(System.currentTimeMillis(), State.DISABLED, emptyList);
			} else {
				gitStatus = gitContext.getGitStatus();
			}
			request.setAttribute(GIT_STATUS_REQUEST_CACHE_KEY, gitStatus);
		}
		return gitStatus;
	}
}


/*
 * ao-dbc - Simplified JDBC access for simplified code.
 * Copyright (C) 2011, 2013, 2015, 2016  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-dbc.
 *
 * ao-dbc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-dbc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-dbc.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.dbc.meta;

import com.aoindustries.dbc.NoRowException;
import com.aoindustries.util.AoCollections;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * A friendly wrapper around database meta data.
 *
 * @author  AO Industries, Inc.
 */
public class Schema {

	private final Catalog catalog;
	private final String name;
	private final int hashCode;

	protected Schema(Catalog catalog, String name) {
		this.catalog = catalog;
		if(name.indexOf('"')!=-1) throw new IllegalArgumentException();
		this.name = name;
		this.hashCode = name.hashCode();
	}

	@Override
	public String toString() {
		return name;
	}

	/**
	 * Two schemas are equal if they have the same name.
	 */
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof Schema)) return false;
		Schema other = (Schema)obj;
		return
			hashCode==other.hashCode
			&& name.equals(other.name)
		;
	}

	@Override
	public int hashCode() {
		return hashCode;
	}

	public Catalog getCatalog() {
		return catalog;
	}

	public String getName() {
		return name;
	}

	private static class GetTablesLock {}
	private final GetTablesLock getTablesLock = new GetTablesLock();
	private SortedMap<String,Table> getTablesCache;

	/**
	 * Gets all tables for this schema keyed by unique name.
	 *
	 * @see  java.sql.DatabaseMetaData#getTables()
	 */
	public SortedMap<String,Table> getTables() throws SQLException {
		synchronized(getTablesLock) {
			if(getTablesCache==null) {
				SortedMap<String,Table> newTables = new TreeMap<>(DatabaseMetaData.getCollator());
				try (ResultSet results = catalog.getMetaData().getMetaData().getTables(catalog.getName(), name, null, null)) {
					while(results.next()) {
						Table newTable = new Table(this, results.getString("TABLE_NAME"), results.getString("TABLE_TYPE"));
						if(newTables.put(newTable.getName(), newTable)!=null) throw new AssertionError("Duplicate table: "+newTable);
					}
				}
				getTablesCache = AoCollections.optimalUnmodifiableSortedMap(newTables);
			}
			return getTablesCache;
		}
	}

	/**
	 * Gets the table of the provided name.
	 *
	 * @throws  NoRowException if the table doesn't exist
	 */
	public Table getTable(String name) throws NoRowException, SQLException {
		Table table = getTables().get(name);
		if(table==null) throw new NoRowException();
		return table;
	}
}


/*
 * ao-dbc - Simplified JDBC access for simplified code.
 * Copyright (C) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-dbc.
 *
 * ao-dbc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-dbc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-dbc.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.dbc;

import com.aoindustries.sql.SQLUtility;
import com.aoindustries.sql.WrappedSQLException;
import com.aoindustries.util.IntArrayList;
import com.aoindustries.util.IntList;
import com.aoindustries.util.LongArrayList;
import com.aoindustries.util.LongList;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLXML;
import java.sql.Struct;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Collection;
import java.util.logging.Level;

/**
 * A DatabaseConnection is used to only get actual database connections when needed.
 *
 * @see  Database
 *
 * @author  AO Industries, Inc.
 */
public class DatabaseConnection extends AbstractDatabaseAccess {

	private static final int FETCH_SIZE = 1000;

	/**
	 * Gets a user-friendly description of the provided result in a string formatted like
	 * ('value', 'value', int_value, ...).  This must not be used generate
	 * SQL statements - it is just to provider user display.
	 */
	@SuppressWarnings("deprecation")
	private static String getRow(ResultSet result) throws SQLException {
		StringBuilder sb = new StringBuilder();
		sb.append('(');
		ResultSetMetaData metaData = result.getMetaData();
		int colCount = metaData.getColumnCount();
		for(int c=1; c<=colCount; c++) {
			if(c>1) sb.append(", ");
			int colType = metaData.getColumnType(c);
			switch(colType) {
				case Types.BIGINT :
				case Types.BIT :
				case Types.BOOLEAN :
				case Types.DECIMAL :
				case Types.DOUBLE :
				case Types.FLOAT :
				case Types.INTEGER :
				case Types.NUMERIC :
				case Types.REAL :
				case Types.SMALLINT :
				case Types.TINYINT :
					sb.append(result.getObject(c));
					break;
				case Types.CHAR :
				case Types.DATE :
				case Types.LONGNVARCHAR :
				case Types.LONGVARCHAR :
				case Types.NCHAR :
				case Types.NVARCHAR :
				case Types.TIME :
				case Types.TIMESTAMP :
				case Types.VARCHAR :
				default :
					sb.append('\'');
					SQLUtility.escapeSQL(result.getString(c), sb);
					sb.append('\'');
					break;
				//default :
				//    throw new SQLException("Unexpected column type: "+colType);
			}
		}
		sb.append(')');
		return sb.toString();
	}

	private final Database database;

	Connection _conn;

	protected DatabaseConnection(Database database) {
	   this.database=database;
	}

	public Database getDatabase() {
		return database;
	}

	public Connection getConnection(int isolationLevel, boolean readOnly) throws SQLException {
		return getConnection(isolationLevel, readOnly, 1);
	}

	public Connection getConnection(int isolationLevel, boolean readOnly, int maxConnections) throws SQLException {
		Connection c=_conn;
		if(c==null) {
			c=database.getConnection(isolationLevel, readOnly, maxConnections);
			if(!readOnly || isolationLevel>=Connection.TRANSACTION_REPEATABLE_READ) c.setAutoCommit(false);
			_conn=c;
		} else if(c.getTransactionIsolation()<isolationLevel) {
			if(!c.getAutoCommit()) {
				// c.commit();
				c.setAutoCommit(true);
			}
			c.setTransactionIsolation(isolationLevel);
			if(!readOnly && c.isReadOnly()) c.setReadOnly(false);
			if(!readOnly || isolationLevel>=Connection.TRANSACTION_REPEATABLE_READ) c.setAutoCommit(false);
		} else if(!readOnly && c.isReadOnly()) {
			if(!c.getAutoCommit()) {
				// May be able to get rid of the commit - setAutoCommit should commit according to the documentation
				// c.commit();
				c.setAutoCommit(true);
			}
			c.setReadOnly(false);
			c.setAutoCommit(false);
		}
		return c;
	}

	protected static void setParam(Connection conn, PreparedStatement pstmt, int pos, Object param) throws SQLException {
		if(param==null) pstmt.setNull(pos, Types.VARCHAR);
		else if(param instanceof Null) pstmt.setNull(pos, ((Null)param).getType());
		else if(param instanceof Array) pstmt.setArray(pos, (Array)param);
		else if(param instanceof BigDecimal) pstmt.setBigDecimal(pos, (BigDecimal)param);
		else if(param instanceof BigInteger) pstmt.setBigDecimal(pos, new BigDecimal((BigInteger)param));
		else if(param instanceof Blob) pstmt.setBlob(pos, (Blob)param);
		else if(param instanceof Boolean) pstmt.setBoolean(pos, (Boolean)param);
		else if(param instanceof Byte) pstmt.setByte(pos, (Byte)param);
		else if(param instanceof byte[]) pstmt.setBytes(pos, (byte[])param);
		else if(param instanceof Clob) pstmt.setClob(pos, (Clob)param);
		else if(param instanceof Date) pstmt.setDate(pos, (Date)param);
		else if(param instanceof Double) pstmt.setDouble(pos, (Double)param);
		else if(param instanceof Float) pstmt.setFloat(pos, (Float)param);
		else if(param instanceof Integer) pstmt.setInt(pos, (Integer)param);
		else if(param instanceof InputStream) pstmt.setBinaryStream(pos, (InputStream)param);
		else if(param instanceof Long) pstmt.setLong(pos, (Long)param);
		else if(param instanceof NClob) pstmt.setNClob(pos, (NClob)param);
		else if(param instanceof Reader) pstmt.setCharacterStream(pos, (Reader)param);
		else if(param instanceof Ref) pstmt.setRef(pos, (Ref)param);
		else if(param instanceof RowId) pstmt.setRowId(pos, (RowId)param);
		else if(param instanceof Short) pstmt.setShort(pos, (Short)param);
		else if(param instanceof SQLXML) pstmt.setSQLXML(pos, (SQLXML)param);
		else if(param instanceof String) pstmt.setString(pos, (String)param);
		else if(param instanceof Time) pstmt.setTime(pos, (Time)param);
		else if(param instanceof Timestamp) pstmt.setTimestamp(pos, (Timestamp)param);
		else if(param instanceof URL) pstmt.setURL(pos, (URL)param);
		else if(param instanceof Enum) pstmt.setString(pos, ((Enum)param).name());
		else if(
			(param instanceof SQLData)
			|| (param instanceof Struct)
		) pstmt.setObject(pos, param);
		else if(param instanceof String[]) {
			pstmt.setArray(pos, conn.createArrayOf("text", (Object[])param));
		} else {
			// Defaults to string with object.toString only when the class has a valueOf(String) method that will reconstitute it in AutoObjectFactory
			Class<?> clazz = param.getClass();
			if(AutoObjectFactory.getValueOfStringMethod(clazz)!=null) pstmt.setString(pos, param.toString());
			else {
				// Call setObject here, to give the database driver a chance to decide what to do
				pstmt.setObject(pos, param);
				//throw new SQLException("Unexpected parameter class: "+clazz.getName());
			}
		}
	}

	public static void setParams(Connection conn, PreparedStatement pstmt, Object ... params) throws SQLException {
		int pos=1;
		for(Object param : params) setParam(conn, pstmt, pos++, param);
	}

	@Override
	public boolean executeBooleanQuery(int isolationLevel, boolean readOnly, boolean rowRequired, String sql, Object ... params) throws NoRowException, SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					if(results.next()) {
						boolean b=results.getBoolean(1);
						if(results.next()) throw new ExtraRowException();
						return b;
					}
					if(rowRequired) throw new NoRowException();
					return false;
				}
			} catch(NoRowException err) {
				throw err;
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public IntList executeIntListQuery(int isolationLevel, boolean readOnly, String sql, Object ... params) throws SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		conn.setAutoCommit(false);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				pstmt.setFetchSize(FETCH_SIZE);
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					IntList V=new IntArrayList();
					while(results.next()) V.add(results.getInt(1));
					return V;
				}
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public int executeIntQuery(int isolationLevel, boolean readOnly, boolean rowRequired, String sql, Object ... params) throws NoRowException, SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					if(results.next()) {
						int i=results.getInt(1);
						if(results.next()) throw new ExtraRowException();
						return i;
					}
					if(rowRequired) throw new NoRowException();
					return 0;
				}
			} catch(NoRowException err) {
				throw err;
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public LongList executeLongListQuery(int isolationLevel, boolean readOnly, String sql, Object ... params) throws SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		conn.setAutoCommit(false);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				pstmt.setFetchSize(FETCH_SIZE);
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					LongList V=new LongArrayList();
					while(results.next()) V.add(results.getLong(1));
					return V;
				}
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public long executeLongQuery(int isolationLevel, boolean readOnly, boolean rowRequired, String sql, Object ... params) throws NoRowException, SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					if(results.next()) {
						long l=results.getLong(1);
						if(results.next()) throw new ExtraRowException();
						return l;
					}
					if(rowRequired) throw new NoRowException();
					return 0;
				}
			} catch(NoRowException err) {
				throw err;
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public <T,E extends Exception> T executeObjectQuery(int isolationLevel, boolean readOnly, boolean rowRequired, Class<E> eClass, ObjectFactoryE<T,E> objectFactory, String sql, Object ... params) throws NoRowException, SQLException, E {
		Connection conn = getConnection(isolationLevel, readOnly);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					if(results.next()) {
						T object = objectFactory.createObject(results);
						if(results.next()) throw new ExtraRowException();
						return object;
					}
					if(rowRequired) throw new NoRowException();
					return null;
				}
			} catch(NoRowException err) {
				throw err;
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public <T,C extends Collection<? super T>,E extends Exception> C executeObjectCollectionQuery(int isolationLevel, boolean readOnly, C collection, Class<E> eClass, ObjectFactoryE<T,E> objectFactory, String sql, Object ... params) throws SQLException, E {
		Connection conn = getConnection(isolationLevel, readOnly);
		conn.setAutoCommit(false);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				pstmt.setFetchSize(FETCH_SIZE);
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					while(results.next()) {
						T newObj = objectFactory.createObject(results);
						if(!collection.add(newObj)) throw new SQLException("Duplicate row in results: "+getRow(results));
					}
					return collection;
				}
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public <T,E extends Exception> T executeQuery(int isolationLevel, boolean readOnly, Class<E> eClass, ResultSetHandlerE<T,E> resultSetHandler, String sql, Object ... params) throws SQLException, E {
		Connection conn = getConnection(isolationLevel, readOnly);
		conn.setAutoCommit(false);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				pstmt.setFetchSize(FETCH_SIZE);
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					return resultSetHandler.handleResultSet(results);
				}
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public short executeShortQuery(int isolationLevel, boolean readOnly, boolean rowRequired, String sql, Object ... params) throws NoRowException, SQLException {
		Connection conn = getConnection(isolationLevel, readOnly);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				try (ResultSet results = pstmt.executeQuery()) {
					if(results.next()) {
						short s=results.getShort(1);
						if(results.next()) throw new ExtraRowException();
						return s;
					}
					if(rowRequired) throw new NoRowException();
					return 0;
				}
			} catch(NoRowException err) {
				throw err;
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	@Override
	public int executeUpdate(String sql, Object ... params) throws SQLException {
		Connection conn = getConnection(Connection.TRANSACTION_READ_COMMITTED, false);
		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
			try {
				setParams(conn, pstmt, params);
				return pstmt.executeUpdate();
			} catch(SQLException err) {
				throw new WrappedSQLException(err, pstmt);
			}
		}
	}

	public void commit() throws SQLException {
		Connection c=_conn;
		if(c!=null && !c.getAutoCommit()) c.commit();
	}

	public boolean isClosed() throws SQLException {
		Connection c=_conn;
		return c==null || c.isClosed();
	}

	public void releaseConnection() throws SQLException {
		Connection c=_conn;
		if(c!=null) {
			_conn=null;
			database.releaseConnection(c);
		}
	}

	public boolean rollback() {
		boolean rolledBack=false;
		try {
			if(_conn!=null && !_conn.isClosed()) {
				rolledBack=true;
				if(!_conn.getAutoCommit()) _conn.rollback();
			}
		} catch(SQLException err) {
			database.getLogger().logp(Level.SEVERE, DatabaseConnection.class.getName(), "rollback", null, err);
		}
		return rolledBack;
	}

	public boolean rollbackAndClose() {
		boolean rolledBack=false;
		try {
			if(_conn!=null && !_conn.isClosed()) {
				rolledBack=true;
				if(!_conn.getAutoCommit()) _conn.rollback();
				_conn.close();
			}
		} catch(SQLException err) {
			database.getLogger().logp(Level.SEVERE, DatabaseConnection.class.getName(), "rollbackAndClose", null, err);
		}
		return rolledBack;
	}
}


/*
 * ao-io-buffer - Output buffering library.
 * Copyright (C) 2013, 2015, 2016, 2017, 2019  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-io-buffer.
 *
 * ao-io-buffer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-io-buffer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-io-buffer.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.io.buffer;

import com.aoindustries.io.Encoder;
import com.aoindustries.io.EncoderWriter;
import com.aoindustries.io.IoUtils;
import com.aoindustries.tempfiles.TempFile;
import com.aoindustries.util.BufferManager;
import com.aoindustries.util.WrappedException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * {@inheritDoc}
 *
 * This class is not thread safe.
 *
 * TODO: Performance: When reading blocks, align to block boundary instead of remaining offset by start / writeStart.
 *
 * @author  AO Industries, Inc.
 */
public class TempFileResult implements BufferResult {

	private static final Logger logger = Logger.getLogger(TempFileResult.class.getName());

	private final TempFile tempFile;

	private final long start;
	private final long end;

	protected TempFileResult(
		TempFile tempFile,
		long start,
		long end
	) {
		this.tempFile = tempFile;
		this.start = start;
		this.end = end;
	}

	@Override
	public long getLength() {
		return end - start;
	}

	private String toStringCache;

	@Override
	public boolean isFastToString() {
		return toStringCache!=null;
	}

	@Override
	public String toString() {
		if(toStringCache==null) {
			try {
				if(logger.isLoggable(Level.INFO)) {
					logger.log(
						Level.INFO,
						"Creating String from temp file - benefits of buffering negated.",
						new Throwable("Stack Trace")
					);
				}
				final long length = end - start;
				if(length>Integer.MAX_VALUE) throw new RuntimeException("Buffer too large to convert to String: length="+length);
				StringBuilder sb = new StringBuilder((int)length);
				try (RandomAccessFile raf = new RandomAccessFile(tempFile.getFile(), "r")) {
					byte[] bytes = BufferManager.getBytes();
					try {
						long index = this.start;
						raf.seek(index<<1);
						while(index<end) {
							// Read a block
							long blockSizeLong = (end - index)<<1;
							int blockSize = blockSizeLong > BufferManager.BUFFER_SIZE ? BufferManager.BUFFER_SIZE : (int)blockSizeLong;
							assert (blockSize&1) == 0 : "Must be an even number for UTF-16 conversion";
							raf.readFully(bytes, 0, blockSize);
							// Convert to characters in sb
							for(int bpos=0; bpos<blockSize; bpos+=2) {
								sb.append(IoUtils.bufferToChar(bytes, bpos));
							}
							// Update location
							index += blockSize>>1;
						}
					} finally {
						BufferManager.release(bytes, false);
					}
				}
				assert sb.length()==length : "sb.length()!=length: "+sb.length()+"!="+length;
				toStringCache = sb.toString();
			} catch(IOException err) {
				throw new WrappedException(err);
			}
		}
		return toStringCache;
	}

	@Override
	public void writeTo(Writer out) throws IOException {
		writeToImpl(out, start, end);
	}

	@Override
	public void writeTo(Writer out, long off, long len) throws IOException {
		if((start + off + len) > end) throw new IndexOutOfBoundsException();
		writeToImpl(out, start + off, start + off + len);
	}

	@Override
	public void writeTo(Encoder encoder, Writer out) throws IOException {
		writeTo(
			encoder!=null
				? new EncoderWriter(encoder, out)
				: out
		);
	}

	@Override
	public void writeTo(Encoder encoder, Writer out, long off, long len) throws IOException {
		writeTo(
			encoder!=null
				? new EncoderWriter(encoder, out)
				: out,
			off,
			len
		);
	}

	/**
	 * Implementation of writeTo
	 *
	 * @param out
	 * @param writeStart  The absolute index to write from
	 * @param writeEnd    The absolute index one past last character to write
	 * @throws IOException
	 */
	private void writeToImpl(Writer out, long writeStart, long writeEnd) throws IOException {
		try ( // TODO: If copying to another SegmentedBufferedWriter or AutoTempFileWriter, we have a chance here for disk-to-disk block level copying instead of going through all the conversions.
			RandomAccessFile raf = new RandomAccessFile(tempFile.getFile(), "r")) {
			byte[] bytes = BufferManager.getBytes();
			try {
				char[] chars = BufferManager.getChars();
				try {
					long index = writeStart;
					raf.seek(index<<1);
					while(index<writeEnd) {
						// Read a block
						long blockSizeLong = (writeEnd - index)<<1;
						int blockSize = blockSizeLong > BufferManager.BUFFER_SIZE ? BufferManager.BUFFER_SIZE : (int)blockSizeLong;
						assert (blockSize&1) == 0 : "Must be an even number for UTF-16 conversion";
						raf.readFully(bytes, 0, blockSize);
						// Convert to characters
						for(
							int bpos=0, cpos=0;
							bpos<blockSize;
							bpos+=2, cpos++
						) {
							chars[cpos] = IoUtils.bufferToChar(bytes, bpos);
						}
						// Write to output
						out.write(chars, 0, blockSize>>1);
						// Update location
						index += blockSize>>1;
					}
				} finally {
					BufferManager.release(chars, false);
				}
			} finally {
				BufferManager.release(bytes, false);
			}
		}
	}

	@Override
	public BufferResult trim() throws IOException {
		// Trim from temp file
		long newStart;
		long newEnd;
		try (RandomAccessFile raf = new RandomAccessFile(tempFile.getFile(), "r")) {
			newStart = this.start;
			// Skip past the beginning whitespace characters
			raf.seek(newStart<<1);
			while(newStart<end) {
				char ch = raf.readChar();
				if(!Character.isWhitespace(ch)) break;
				newStart++;
			}
			// Skip past the ending whitespace characters
			newEnd = end;
			while(newEnd>newStart) {
				raf.seek((newEnd-1)<<1);
				char ch = raf.readChar();
				if(!Character.isWhitespace(ch)) break;
				newEnd--;
			}
		}
		// Keep this object if already trimmed
		if(
			start==newStart
			&& end==newEnd
		) {
			return this;
		} else {
			// Check if empty
			if(newStart==newEnd) {
				return EmptyResult.getInstance();
			} else {
				// Otherwise, return new substring
				return new TempFileResult(
					tempFile,
					newStart,
					newEnd
				);
			}
		}
	}
}


/*
 * ao-dbc - Simplified JDBC access for simplified code.
 * Copyright (C) 2011, 2013, 2015, 2016  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-dbc.
 *
 * ao-dbc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-dbc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-dbc.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.dbc.meta;

import com.aoindustries.dbc.NoRowException;
import com.aoindustries.util.AoCollections;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.Collator;
import java.util.Locale;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * A friendly wrapper around database meta data.
 *
 * @author  AO Industries, Inc.
 */
public class DatabaseMetaData {

	private static final Collator englishCollator = Collator.getInstance(Locale.ENGLISH);

	/**
	 * Gets the collator used for result ordering.
	 */
	public static Collator getCollator() {
		return englishCollator;
	}

	private final java.sql.DatabaseMetaData metaData;

	/**
	 * Creates a new meta data wrapper.  Results are cached, so for a fresh view of
	 * meta data, create a new instance.
	 */
	public DatabaseMetaData(Connection conn) throws SQLException {
		this(conn.getMetaData());
	}

	/**
	 * Creates a new meta data wrapper.  Results are cached, so for a fresh view of
	 * meta data, create a new instance.
	 */
	public DatabaseMetaData(java.sql.DatabaseMetaData metaData) {
		this.metaData = metaData;
	}

	@Override
	public String toString() {
		try {
			String url = metaData.getURL();
			if(url!=null) return url;
		} catch(SQLException exc) {
			// Ignored
		}
		return super.toString();
	}

	public java.sql.DatabaseMetaData getMetaData() {
		return metaData;
	}

	private static class GetCatalogsLock {}
	private final GetCatalogsLock getCatalogsLock = new GetCatalogsLock();
	private SortedMap<String,Catalog> getCatalogsCache;

	/**
	 * Gets all catalogs for this database keyed by unique name.
	 *
	 * @see  java.sql.DatabaseMetaData#getCatalogs()
	 */
	public SortedMap<String,Catalog> getCatalogs() throws SQLException {
		synchronized(getCatalogsLock) {
			if(getCatalogsCache==null) {
				SortedMap<String,Catalog> newCatalogs = new TreeMap<>(englishCollator);
				try (ResultSet results = metaData.getCatalogs()) {
					while(results.next()) {
						Catalog newCatalog = new Catalog(this, results.getString(1));
						if(newCatalogs.put(newCatalog.getName(), newCatalog)!=null) throw new AssertionError("Duplicate catalog: "+newCatalog);
					}
				}
				getCatalogsCache = AoCollections.optimalUnmodifiableSortedMap(newCatalogs);
			}
			return getCatalogsCache;
		}
	}

	/**
	 * Gets the catalog of the provided name.
	 *
	 * @throws  NoRowException if the catalog doesn't exist
	 */
	public Catalog getCatalog(String name) throws NoRowException, SQLException {
		Catalog catalog = getCatalogs().get(name);
		if(catalog==null) throw new NoRowException();
		return catalog;
	}
}


/*
 * ao-io-buffer - Output buffering library.
 * Copyright (C) 2010, 2011, 2012, 2013, 2015, 2016, 2017  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-io-buffer.
 *
 * ao-io-buffer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-io-buffer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-io-buffer.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.io.buffer;

import com.aoindustries.util.AoArrays;
import com.aoindustries.util.BufferManager;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.Arrays;

/**
 * Writes to a set of internally managed buffers.  When possible, the buffers are reused.
 *

 * Maximum length is 2 ^ 30 characters (about 1 billion).
 * When this limit is insufficient, consider using along with
 * {@link AutoTempFileWriter}.
 *

 *

 * This class is not thread safe.
 *

 *
 * @see  SegmentedBufferedWriter  for a possibly more efficient implementation.
 *
 * @author  AO Industries, Inc.
 */
public class CharArrayBufferWriter extends BufferWriter {

	/**
	 * The maximum buffer length is 2^30 due to array length limitations.
	 */
	private static final int MAX_LENGTH = 1 << 30;

	/**
	 * Any buffer under this size will be copied into a new array on close in
	 * order to reduce memory consumption and recycle the initial buffer.
	 */
	private static final int COPY_THEN_RECYCLE_LIMIT = BufferManager.BUFFER_SIZE / 8;

	/**
	 * The length of the writer is the sum of the data written to the buffer.
	 * Once closed, this length will not be modified.
	 */
	private int length = 0;

	/**
	 * The buffer used to capture data.
	 * Once closed, this buffer will not be modified.
	 *
	 * TODO: Consider a set of buffers to avoid copying on resize.
	 *       This would also allow beyond 32-bit limit.
	 */
	private char[] buffer = AoArrays.EMPTY_CHAR_ARRAY;

	/**
	 * Once closed, no further information may be written.
	 * Manipulations are only active once closed.
	 */
	private boolean isClosed = false;

	public CharArrayBufferWriter() {
	}

	/**
	 * Grows as-needed to fit the provided new capacity.
	 *
	 * @returns the possibly new buffer.
	 */
	private char[] getBuffer(int additional) throws IOException {
		long newLen = (long)length + additional;
		if(newLen > MAX_LENGTH) throw new IOException("Maximum buffer length is " + MAX_LENGTH + ", " + newLen + " requested");
		char[] buf = this.buffer;
		int bufLen = buf.length;
		if(newLen > bufLen) {
			// Find the next power of two that will hold all of the contents
			int newBufLen = bufLen==0 ? BufferManager.BUFFER_SIZE : (bufLen << 1);
			while(newBufLen < newLen) {
				newBufLen <<= 1;
			}
			char[] newBuf =
				(newBufLen == BufferManager.BUFFER_SIZE)
				? BufferManager.getChars()
				: new char[newBufLen];
			System.arraycopy(buf, 0, newBuf, 0, length);
			// Recycle buffer
			if(bufLen == BufferManager.BUFFER_SIZE) {
				BufferManager.release(buf, false);
			}
			buf = newBuf;
			this.buffer = buf;
		}
		return buf;
	}

	@Override
	public void write(int c) throws IOException {
		if(isClosed) throw new ClosedChannelException();
		getBuffer(1)[length++] = (char)c;
	}

	@Override
	public void write(char cbuf[], int off, int len) throws IOException {
		if(isClosed) throw new ClosedChannelException();
		if(len > 0) {
			char[] buf = getBuffer(len);
			System.arraycopy(cbuf, off, buf, length, len);
			length += len;
		}
	}

	@Override
	public void write(String str, int off, int len) throws IOException {
		if(isClosed) throw new ClosedChannelException();
		if(len > 0) {
			char[] buf = getBuffer(len);
			str.getChars(off, off + len, buf, length);
			length += len;
		}
	}

	@Override
	public CharArrayBufferWriter append(CharSequence csq) throws IOException {
		super.append(csq);
		return this;
	}

	@Override
	public CharArrayBufferWriter append(CharSequence csq, int start, int end) throws IOException {
		if(csq == null) {
			write("null");
		} else {
			if(csq instanceof String) {
				// Avoid subSequence which copies characters in Java 1.8+
				write((String)csq, start, end - start);
			} else {
		        write(csq.subSequence(start, end).toString());
			}
		}
		return this;
	}

	@Override
	public CharArrayBufferWriter append(char c) throws IOException {
		super.append(c);
		return this;
	}

	@Override
	public void flush() throws IOException {
		if(isClosed) throw new ClosedChannelException();
		// Nothing to do
	}

	@Override
	public void close() throws IOException {
		isClosed = true;
		int len = this.length;
		if(len > 0) {
			if(len <= COPY_THEN_RECYCLE_LIMIT) {
				char[] oldBuf = buffer;
				this.buffer = Arrays.copyOf(oldBuf, len);
				if(oldBuf.length == BufferManager.BUFFER_SIZE) {
					BufferManager.release(oldBuf, false);
				}
			}
		}
	}

	@Override
	public long getLength() {
		return length;
	}

	@Override
	public String toString() {
		return "CharArrayBufferWriter(length=" + length + ")";
	}

	// The result is cached after first created
	private BufferResult result;

	@Override
	public BufferResult getResult() throws IllegalStateException {
		if(!isClosed) throw new IllegalStateException();
		if(result==null) {
			if(length == 0) {
				result = EmptyResult.getInstance();
			} else {
				result = new CharArrayBufferResult(buffer, 0, length);
			}
		}
		return result;
	}
}


/*
 * ao-io-buffer - Output buffering library.
 * Copyright (C) 2013, 2015, 2016  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-io-buffer.
 *
 * ao-io-buffer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-io-buffer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-io-buffer.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.io.buffer;

import com.aoindustries.io.Encoder;
import com.aoindustries.io.EncoderWriter;
import com.aoindustries.util.AtomicSequence;
import com.aoindustries.util.Sequence;
import com.aoindustries.util.WrappedException;
import java.io.IOException;
import java.io.Writer;

/**
 * Logs all write calls in a way that can be put into Java source code.
 * This is used to capture real-world scenarios for unit testing.
 *
 * This class is not thread safe.
 *
 * @author  AO Industries, Inc.
 */
public class LoggingResult implements BufferResult {

	private static final Sequence idSeq = new AtomicSequence(0);

	final long id = idSeq.getNextSequenceValue();
	private final BufferResult wrapped;
	private final Writer log;

	protected LoggingResult(BufferResult wrapped, Writer log) throws IOException {
		this.wrapped = wrapped;
		this.log = log;
	}

	public long getId() {
		return id;
	}

	/**
	 * Provides detailed logging for a media encoder.
	 */
	private void log(Encoder encoder) throws IOException {
		if(encoder==null) log.write("null");
		else {
			String className = encoder.getClass().getName();
			// Some shortcuts from the ao-encoding project, classnames used here to avoid hard dependency
			if("com.aoindustries.encoding.JavaScriptInXhtmlAttributeEncoder".equals(className)) {
				log.write("javaScriptInXhtmlAttributeEncoder");
			} else if("com.aoindustries.encoding.JavaScriptInXhtmlEncoder".equals(className)) {
				log.write("javaScriptInXhtmlEncoder");
			} else if("com.aoindustries.encoding.TextInXhtmlAttributeEncoder".equals(className)) {
				log.write("textInXhtmlAttributeEncoder");
			} else {
				log.write(className);
			}
		}
	}

	/**
	 * Provides detailed logging for a writer.
	 */
	private void log(Writer writer) throws IOException {
		if(writer==null) {
			log.write("null");
		} else if(writer instanceof LoggingWriter) {
			LoggingWriter loggingWriter = (LoggingWriter)writer;
			log.write("writer[");
			log.write(Long.toString(loggingWriter.getId()));
			log.write(']');
		} else if(writer instanceof EncoderWriter) {
			EncoderWriter encoderWriter = (EncoderWriter)writer;
			log.write("new EncoderWriter(");
			log(encoderWriter.getEncoder());
			log.write(", ");
			log(encoderWriter.getOut());
			log.write(')');
		} else {
			String classname = writer.getClass().getName();
			if(classname.equals("org.apache.jasper.runtime.BodyContentImpl")) log.write("bodyContent");
			else if(classname.equals("org.apache.jasper.runtime.JspWriterImpl")) log.write("jspWriter");
			else log.write(classname);
		}
	}

	@Override
	public long getLength() throws IOException {
		log.write("result[");
		log.write(Long.toString(id));
		log.write("].getLength();\n");
		log.flush();
		return wrapped.getLength();
	}

	@Override
	public boolean isFastToString() {
		return wrapped.isFastToString();
	}

	@Override
	public String toString() {
		try {
			log.write("result[");
			log.write(Long.toString(id));
			log.write("].toString();\n");
			log.flush();
		} catch(IOException e) {
			throw new WrappedException(e);
		}
		return wrapped.toString();
	}

	@Override
	public void writeTo(Writer out) throws IOException {
		log.write("result[");
		log.write(Long.toString(id));
		log.write("].writeTo(");
		log(out);
		log.write(");\n");
		log.flush();
		wrapped.writeTo(out);
	}

	@Override
	public void writeTo(Writer out, long off, long len) throws IOException {
		log.write("result[");
		log.write(Long.toString(id));
		log.write("].writeTo(");
		log(out);
		log.write(", ");
		log.write(Long.toString(off));
		log.write(", ");
		log.write(Long.toString(len));
		log.write(");\n");
		log.flush();
		wrapped.writeTo(out, off, len);
	}

	@Override
	public void writeTo(Encoder encoder, Writer out) throws IOException {
		log.write("result[");
		log.write(Long.toString(id));
		log.write("].writeTo(");
		log(encoder);
		log.write(", ");
		log(out);
		log.write(");\n");
		log.flush();
		wrapped.writeTo(encoder, out);
	}

	@Override
	public void writeTo(Encoder encoder, Writer out, long off, long len) throws IOException {
		log.write("result[");
		log.write(Long.toString(id));
		log.write("].writeTo(");
		log(encoder);
		log.write(", ");
		log(out);
		log.write(", ");
		log.write(Long.toString(off));
		log.write(", ");
		log.write(Long.toString(len));
		log.write(");\n");
		log.flush();
		wrapped.writeTo(encoder, out, off, len);
	}

	@Override
	public LoggingResult trim() throws IOException {
		LoggingResult result = new LoggingResult(wrapped.trim(), log);
		log.write("result[");
		log.write(Long.toString(result.id));
		log.write("] = result[");
		log.write(Long.toString(id));
		log.write("].trim();\n");
		log.flush();
		return result;
	}
}


/*
 * Copyright © 2017 <code@io7m.com> http://io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.jlexing.core;

import com.io7m.junreachable.UnreachableCodeException;

import java.util.Optional;

/**
 * Convenience functions over lexical positions.
 *
 * @since 1.1.0
 */

public final class LexicalPositions
{
  private LexicalPositions()
  {
    throw new UnreachableCodeException();
  }

  /**
   * Construct a lexical position at line 0, column 0, with no file.
   *
   * @param <F> The type of file names
   *
   * @return A lexical position
   */

  public static <F> LexicalPosition<F> zero()
  {
    return LexicalPosition.of(0, 0, Optional.empty());
  }

  /**
   * Construct a lexical position at line 0, column 0, with the given file.
   *
   * @param file The type name
   * @param <F>  The type of file names
   *
   * @return A lexical position
   */

  public static <F> LexicalPosition<F> zeroWithFile(
    final F file)
  {
    return LexicalPosition.of(0, 0, Optional.of(file));
  }
}


/*
 * ao-io-buffer - Output buffering library.
 * Copyright (C) 2013, 2015, 2016, 2019  AO Industries, Inc.
 *     support@aoindustries.com
 *     7262 Bull Pen Cir
 *     Mobile, AL 36695
 *
 * This file is part of ao-io-buffer.
 *
 * ao-io-buffer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ao-io-buffer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ao-io-buffer.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aoindustries.io.buffer;

import com.aoindustries.io.Encoder;
import java.io.IOException;
import java.io.Writer;
import java.util.logging.Logger;

/**
 * {@inheritDoc}
 *
 * This class is not thread safe.
 *
 * @author  AO Industries, Inc.
 */
public class SegmentedResult implements BufferResult {

	private static final Logger logger = Logger.getLogger(SegmentedResult.class.getName());

	/**
	 * @see  SegmentedWriter#segmentTypes
	 */
	private final byte[] segmentTypes;
	private final Object[] segmentValues;
	private final int[] segmentOffsets;
	private final int[] segmentLengths;

	/**
	 * When segments are trimmed (or other types of substring operations), they
	 * share the underlying segment data.  This is similar to the pre Java 1.7
	 * implementation of String.substring.
	 */
	private final long start;
	private final int startSegmentIndex;
	private final int startSegmentOffset;
	private final int startSegmentLength;
	private final long end;
	private final int endSegmentIndex;
	private final int endSegmentOffset;
	private final int endSegmentLength;

	private static void assertThingsMakeSense(
		long start,
		int startSegmentIndex,
		int startSegmentOffset,
		int startSegmentLength,
		long end,
		int endSegmentIndex,
		int endSegmentOffset,
		int endSegmentLength
	) {
		assert start <= end;
		assert startSegmentIndex <= endSegmentIndex;
		if(startSegmentIndex == endSegmentIndex) {
			assert startSegmentOffset == endSegmentOffset;
			assert startSegmentLength == endSegmentLength;
		}
	}

	protected SegmentedResult(
		byte[] segmentTypes,
		Object[] segmentValues,
		int[] segmentOffsets,
		int[] segmentLengths,
		long start,
		int startSegmentIndex,
		int startSegmentOffset, // Start offset and length may have been affected by trimming
		int startSegmentLength,
		long end,
		int endSegmentIndex,
		int endSegmentOffset,
		int endSegmentLength // End offset and length may have been affected by trimming
	) {
		assertThingsMakeSense(start, startSegmentIndex, startSegmentOffset, startSegmentLength, end, endSegmentIndex, endSegmentOffset, endSegmentLength);
		this.segmentTypes = segmentTypes;
		this.segmentValues = segmentValues;
		this.segmentOffsets = segmentOffsets;
		this.segmentLengths = segmentLengths;
		this.start = start;
		this.startSegmentIndex = startSegmentIndex;
		this.startSegmentOffset = startSegmentOffset;
		assert startSegmentLength>0 : "All empty results should have been converted to EmptyResult";
		this.startSegmentLength = startSegmentLength;
		this.end = end;
		assert endSegmentIndex >= startSegmentIndex;
		this.endSegmentIndex = endSegmentIndex;
		this.endSegmentOffset = endSegmentOffset;
		assert endSegmentLength>0 : "All empty results should have been converted to EmptyResult";
		this.endSegmentLength = endSegmentLength;
		assert
			endSegmentIndex != startSegmentIndex
			|| (
				startSegmentOffset == endSegmentOffset
				&& startSegmentLength == endSegmentLength
			)
			: "When start and end segments are at the same index, they must have the same offsets and lengths."
		;
	}

	@Override
	public long getLength() {
		return end - start;
	}

	/**
	 * Appends the full segment (with original offset and length) to the buffer.
	 */
	private void append(int segmentIndex, StringBuilder buffer) {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				int off = segmentOffsets[segmentIndex];
				buffer.append(
					(String)segmentValues[segmentIndex],
					off,
					off + segmentLengths[segmentIndex]
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				buffer.append('\n');
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				buffer.append('"');
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				buffer.append('\'');
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				buffer.append(((Character)segmentValues[segmentIndex]).charValue());
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Appends the given range of a segment. to the buffer.
	 */
	private void append(int segmentIndex, int off, int len, StringBuilder buffer) {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				buffer.append(
					(String)segmentValues[segmentIndex],
					off,
					off + len
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert off==0;
				assert len==1;
				buffer.append('\n');
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert off==0;
				assert len==1;
				buffer.append('"');
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert off==0;
				assert len==1;
				buffer.append('\'');
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert off==0;
				assert len==1;
				buffer.append(((Character)segmentValues[segmentIndex]).charValue());
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Writes the full segment (with original offset and length) to the given writer using the given encoder.
	 */
	private void writeSegment(int segmentIndex, Encoder encoder, Writer out) throws IOException {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				encoder.write(
					(String)segmentValues[segmentIndex],
					segmentOffsets[segmentIndex],
					segmentLengths[segmentIndex],
					out
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				encoder.write('\n', out);
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				encoder.write('"', out);
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				encoder.write('\'', out);
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				encoder.write((Character)segmentValues[segmentIndex], out);
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Writes the given range of a segment to the given writer using the given encoder.
	 */
	private void writeSegment(int segmentIndex, int off, int len, Encoder encoder, Writer out) throws IOException {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				encoder.write(
					(String)segmentValues[segmentIndex],
					off,
					len,
					out
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert off==0;
				assert len==1;
				encoder.write('\n', out);
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert off==0;
				assert len==1;
				encoder.write('"', out);
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert off==0;
				assert len==1;
				encoder.write('\'', out);
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert off==0;
				assert len==1;
				encoder.write((Character)segmentValues[segmentIndex], out);
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Writes the full segment (with original offset and length) to the given writer.
	 */
	private void writeSegment(int segmentIndex, Writer out) throws IOException {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				out.write(
					(String)segmentValues[segmentIndex],
					segmentOffsets[segmentIndex],
					segmentLengths[segmentIndex]
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				out.write('\n');
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				out.write('"');
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				out.write('\'');
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert segmentOffsets[segmentIndex]==0;
				assert segmentLengths[segmentIndex]==1;
				out.write((Character)segmentValues[segmentIndex]);
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Writes the given range of a segment to the given writer.
	 */
	private void writeSegment(int segmentIndex, int off, int len, Writer out) throws IOException {
		switch(segmentTypes[segmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				out.write(
					(String)segmentValues[segmentIndex],
					off,
					len
				);
				break;
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert off==0;
				assert len==1;
				out.write('\n');
				break;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert off==0;
				assert len==1;
				out.write('"');
				break;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert off==0;
				assert len==1;
				out.write('\'');
				break;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert off==0;
				assert len==1;
				out.write((Character)segmentValues[segmentIndex]);
				break;
			default :
				throw new AssertionError();
		}
	}

	/**
	 * Gets the character at the given index in a segment.
	 * This is the absolute index, the offset is not added-in.
	 */
	private static char charAt(byte type, Object value, int charIndex) {
		switch(type) {
			case SegmentedWriter.TYPE_STRING :
				return ((String)value).charAt(charIndex);
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert charIndex==0;
				return '\n';
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert charIndex==0;
				return '"';
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert charIndex==0;
				return '\'';
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert charIndex==0;
				return (Character)value;
			default :
				throw new AssertionError();
		}
	}

	private String toStringCache;

	@Override
	public boolean isFastToString() {
		// Already converted, fast now
		if(toStringCache!=null) return true;
		// If more than one segment, can't be fast toString
		if(startSegmentIndex!=endSegmentIndex) return false;
		// Look for one segment shortcut by type
		switch(segmentTypes[startSegmentIndex]) {
			case SegmentedWriter.TYPE_STRING :
				// If offset into string, is not fast (would incur substring)
				if(startSegmentOffset!=0) return false;
				String str = (String)segmentValues[startSegmentIndex];
				// If fast when covers entire string in this segment
				return startSegmentLength==str.length();
			case SegmentedWriter.TYPE_CHAR_NEWLINE :
				assert startSegmentOffset==0;
				assert startSegmentLength==1;
				return true;
			case SegmentedWriter.TYPE_CHAR_QUOTE :
				assert startSegmentOffset==0;
				assert startSegmentLength==1;
				return true;
			case SegmentedWriter.TYPE_CHAR_APOS :
				assert startSegmentOffset==0;
				assert startSegmentLength==1;
				return true;
			case SegmentedWriter.TYPE_CHAR_OTHER :
				assert startSegmentOffset==0;
				assert startSegmentLength==1;
				return true;
			default :
				throw new AssertionError();
		}
	}

	@Override
	public String toString() {
		if(toStringCache==null) {
			if(startSegmentIndex==endSegmentIndex) {
				// Shortcut for one segment
				switch(segmentTypes[startSegmentIndex]) {
					case SegmentedWriter.TYPE_STRING :
						toStringCache = ((String)segmentValues[startSegmentIndex]).substring(startSegmentOffset, startSegmentOffset + startSegmentLength);
						break;
					case SegmentedWriter.TYPE_CHAR_NEWLINE :
						assert startSegmentOffset==0;
						assert startSegmentLength==1;
						toStringCache = "\n";
						break;
					case SegmentedWriter.TYPE_CHAR_QUOTE :
						assert startSegmentOffset==0;
						assert startSegmentLength==1;
						toStringCache = "\"";
						break;
					case SegmentedWriter.TYPE_CHAR_APOS :
						assert startSegmentOffset==0;
						assert startSegmentLength==1;
						toStringCache = "'";
						break;
					case SegmentedWriter.TYPE_CHAR_OTHER :
						assert startSegmentOffset==0;
						assert startSegmentLength==1;
						toStringCache = String.valueOf(((Character)segmentValues[startSegmentIndex]).charValue());
						break;
					default :
						throw new AssertionError();
				}
			} else {
				logger.fine("Creating String from segments - benefits of SegmentedWriter negated.");
				final long length = end - start;
				if(length>Integer.MAX_VALUE) throw new RuntimeException("Buffer too large to convert to String: length="+length);
				StringBuilder buffer = new StringBuilder((int)length);
				// First segment
				append(startSegmentIndex, startSegmentOffset, startSegmentLength, buffer);
				// Middle segments
				for(int i=startSegmentIndex+1; i<endSegmentIndex; i++) {
					append(i, buffer);
				}
				// End segment
				append(endSegmentIndex, endSegmentOffset, endSegmentLength, buffer);
				assert buffer.length()==length : "buffer.length()!=length: "+buffer.length()+"!="+length;
				toStringCache = buffer.toString();
			}
		}
		return toStringCache;
	}

	@Override
	public void writeTo(Writer out) throws IOException {
		// First segment
		writeSegment(
			startSegmentIndex,
			startSegmentOffset,
			startSegmentLength,
			out
		);
		if(endSegmentIndex!=startSegmentIndex) {
			// Middle segments
			int i = startSegmentIndex+1;
			if(i<endSegmentIndex) {
				// if(out instanceof SegmentedWriter) System.err.println("TODO: Perform array copy of middle segments");
				do {
					writeSegment(i++, out);
				} while(i<endSegmentIndex);
			}
			// End segment
			writeSegment(
				endSegmentIndex,
				endSegmentOffset,
				endSegmentLength,
				out
			);
		}
	}

	@Override
	@SuppressWarnings("deprecation")
	public void writeTo(Writer out, long off, long len) throws IOException {
		// Implementation will need to start through beginning
		throw new com.aoindustries.lang.NotImplementedException("Implement when first needed.");
	}

	@Override
	public void writeTo(Encoder encoder, Writer out) throws IOException {
		if(encoder==null) {
			writeTo(out);
		} else {
			// TODO: If copying to another SegmentedWriter, we have a chance here to share segment list (current the StringBuilder)
			// First segment
			writeSegment(
				startSegmentIndex,
				startSegmentOffset,
				startSegmentLength,
				encoder,
				out
			);
			if(endSegmentIndex!=startSegmentIndex) {
				// Middle segments
				for(int i=startSegmentIndex+1; i<endSegmentIndex; i++) {
					writeSegment(i, encoder, out);
				}
				// End segment
				writeSegment(
					endSegmentIndex,
					endSegmentOffset,
					endSegmentLength,
					encoder,
					out
				);
			}
		}
	}

	@Override
	@SuppressWarnings("deprecation")
	public void writeTo(Encoder encoder, Writer out, long off, long len) throws IOException {
		// Implementation will need to start through beginning
		throw new com.aoindustries.lang.NotImplementedException("Implement when first needed.");
	}

	@Override
	public BufferResult trim() throws IOException {
		// Trim from the left
		long newStart = start;
		int newStartSegmentIndex = startSegmentIndex;
		int newStartSegmentOffset = startSegmentOffset;
		int newStartSegmentLength = startSegmentLength;
		long newEnd = end;
		int newEndSegmentIndex = endSegmentIndex;
		int newEndSegmentOffset = endSegmentOffset;
		int newEndSegmentLength = endSegmentLength;
		assertThingsMakeSense(newStart, newStartSegmentIndex, newStartSegmentOffset, newStartSegmentLength, newEnd, newEndSegmentIndex, newEndSegmentOffset, newEndSegmentLength);
		// Skip past the beginning whitespace characters
		TRIM_LEFT :
		while(newStart<newEnd) {
			// Work on one segment
			final byte type = segmentTypes[newStartSegmentIndex];
			final Object value = segmentValues[newStartSegmentIndex];
			// do...while because segments are never empty
			do {
				char ch = charAt(type, value, newStartSegmentOffset);
				if(!Character.isWhitespace(ch)) break TRIM_LEFT;
				newStart++;
				newStartSegmentOffset++;
				newStartSegmentLength--;
				// Also trim end segment numbers if equal to begin segment index
				if(newStartSegmentIndex==newEndSegmentIndex) {
					newEndSegmentOffset++;
					newEndSegmentLength--;
				}
				assertThingsMakeSense(newStart, newStartSegmentIndex, newStartSegmentOffset, newStartSegmentLength, newEnd, newEndSegmentIndex, newEndSegmentOffset, newEndSegmentLength);
				if(newEnd==newStart) break TRIM_LEFT;
			} while(newStartSegmentLength>0);
			// Move to next segment
			newStartSegmentIndex++;
			if(newStartSegmentIndex==newEndSegmentIndex) {
				// Now reached end segment
				newStartSegmentOffset = newEndSegmentOffset;
				newStartSegmentLength = newEndSegmentLength;
			} else {
				// Middle segment
				newStartSegmentOffset = segmentOffsets[newStartSegmentIndex];
				newStartSegmentLength = segmentLengths[newStartSegmentIndex];
			}
			assertThingsMakeSense(newStart, newStartSegmentIndex, newStartSegmentOffset, newStartSegmentLength, newEnd, newEndSegmentIndex, newEndSegmentOffset, newEndSegmentLength);
		}
		// Trim from the right
		if(newEnd>newStart) {
			assert newEndSegmentIndex >= 0;
			// Work on one segment
			byte type = segmentTypes[newEndSegmentIndex];
			Object value = segmentValues[newEndSegmentIndex];
			TRIM_RIGHT :
			do {
				// do...while because segments are never empty
				do {
					char ch = charAt(type, value, newEndSegmentOffset + newEndSegmentLength - 1);
					if(!Character.isWhitespace(ch)) break TRIM_RIGHT;
					newEnd--;
					newEndSegmentLength--;
					// Also trim start segment numbers of equal to end segment index
					if(newStartSegmentIndex==newEndSegmentIndex)  {
						newStartSegmentLength--;
					}
					assertThingsMakeSense(newStart, newStartSegmentIndex, newStartSegmentOffset, newStartSegmentLength, newEnd, newEndSegmentIndex, newEndSegmentOffset, newEndSegmentLength);
					if(newEnd==newStart) break TRIM_RIGHT;
				} while(newEndSegmentLength > 0);
				// Move to previous segment
				newEndSegmentIndex--;
				assert newEndSegmentIndex >= 0 : "Must be non-negative because we have not made it back to newStart yet";
				type = segmentTypes[newEndSegmentIndex];
				value = segmentValues[newEndSegmentIndex];
				if(newEndSegmentIndex==newStartSegmentIndex) {
					// Now reached start segment
					newEndSegmentOffset = newStartSegmentOffset;
					newEndSegmentLength = newStartSegmentLength;
				} else {
					// Middle segment
					newEndSegmentOffset = segmentOffsets[newEndSegmentIndex];
					newEndSegmentLength = segmentLengths[newEndSegmentIndex];
				}
				assertThingsMakeSense(newStart, newStartSegmentIndex, newStartSegmentOffset, newStartSegmentLength, newEnd, newEndSegmentIndex, newEndSegmentOffset, newEndSegmentLength);
			} while(true);
		}

		// Keep this object if already trimmed
		if(
			start==newStart
			&& end==newEnd
		) {
			return this;
		} else {
			// Check if empty
			if(newStart==newEnd) {
				return EmptyResult.getInstance();
			} else {
				// Otherwise, return new substring
				return new SegmentedResult(
					segmentTypes,
					segmentValues,
					segmentOffsets,
					segmentLengths,
					newStart,
					newStartSegmentIndex,
					newStartSegmentOffset,
					newStartSegmentLength,
					newEnd,
					newEndSegmentIndex,
					newEndSegmentOffset,
					newEndSegmentLength
				);
			}
		}
	}
}