public class SqrlClientRequest {
	private static final Logger logger = LoggerFactory.getLogger(SqrlClientRequest.class);

	private static final String NUT_EQUALS = "nut=";

	private final Integer				negotiatedSqrlProtocolVersion;
	private final SqrlNutToken0						nut;
	private final SqrlRequestCommand	clientCommand;
	private final Map<SqrlServerSideKey, byte[]>	requestKeyTableRaw			= new ConcurrentHashMap<>();
	private final Map<SqrlServerSideKey, String>	requestKeyTableBase64	= new ConcurrentHashMap<>();
	private final Set<SqrlRequestOpt> optList = new HashSet<>();

	private final HttpServletRequest	servletRequest;
	private final String				clientParam;
	private final String				serverParam;
	private final String				correlator;

	public SqrlClientRequest(final HttpServletRequest servletRequest, final SqrlPersistence persistence,
			final SqrlConfigOperations configOps) throws SqrlClientRequestProcessingException {
		this.servletRequest = servletRequest;
		this.clientParam = getRequiredParameter(servletRequest, "client");
		setLoggingField(LogField.CLIENT_PARAM, clientParam);
		this.serverParam = getRequiredParameter(servletRequest, "server");
		setLoggingField(LogField.SERVER_PARAM, serverParam);
		this.nut = SqrlNutTokenFactory.unmarshal(extractFromSqrlCsvString(serverParam, NUT_EQUALS), configOps);
		final String decoded = SqrlUtil.base64UrlDecodeDataFromSqrlClientToString(clientParam);
		// parse server - not a name value pair, just the query string we gave
		this.correlator = extractFromSqrlCsvString(serverParam, SqrlClientParam.cor.toString());

		// parse client
		final Map<String, String> clientNameValuePairTable = parseLinesToNameValueMap(decoded);
		// clientVersionString format is 1[,n],[n-m]
		final String clientVersionString = clientNameValuePairTable.get(SqrlClientParam.ver.toString());
		final Collection<Integer> clientVersionsSupported = SqrlVersionUtil
				.parseClientVersionString(clientVersionString);
		final Integer commonProtocolVersion = SqrlVersionUtil
				.findHighestCommonVersion(SqrlConstants.SUPPORTED_SQRL_VERSIONS, clientVersionsSupported);
		if (commonProtocolVersion == null) {
			throw new SqrlClientRequestProcessingException("No common SQRL protocol version found");
		}
		// Sanity check; we probably need to take different actions for different versions so account for that here
		if (!SqrlConstants.SQRL_VERSION_1.equals(commonProtocolVersion)) {
			throw new SqrlClientRequestProcessingException(
					"Unable to process SQRL protcol version " + commonProtocolVersion);
		}
		this.negotiatedSqrlProtocolVersion = commonProtocolVersion;

		// parse opt
		final String optListString = clientNameValuePairTable.get(SqrlClientParam.opt.toString());
		if (SqrlUtil.isNotBlank(optListString)) {
			for (final String optString : optListString.split("~")) {
				try {
					final SqrlRequestOpt clientOpt = SqrlRequestOpt.valueOf(optString);
					if(!optList.add(clientOpt)) {
						logger.debug(formatForLogging("Client sent opt ", clientOpt, " more than once in clientParam",
								clientOpt));
					}
				} catch (final IllegalArgumentException e) {
					throw new SqrlInvalidRequestException(e, "Unknown SQRL client option '", optString, "'");
				}
			}
			setLoggingField(LogField.OPT_LIST, optList.toString());
		}

		// parse keys
		for (final Map.Entry<String, String> entry : parseLinesToNameValueMap(decoded).entrySet()) {
			final SqrlServerSideKey keyType = SqrlServerSideKey.valueOfOrNull(entry.getKey());
			if (keyType == null) {
				continue;
			}
			final String keyBase64 = entry.getValue();
			// Sanity check for a client sending the same key more than once
			if (requestKeyTableRaw.containsKey(keyType)) {
				// Uh oh, is the value different?
				if (requestKeyTableBase64.get(keyType).equals(entry.getValue())) {
					logger.info("Client sent the key " + keyType
							+ " multiple times but with the same value; this should be reported as a minor bug to the client author.  clientParam="
							+ clientParam);
				} else {
					throw new SqrlInvalidRequestException("Client sent the key " + keyType
							+ " multiple times in the same request, each with different values: " + clientParam);
				}
			} else {
				// Store the keys in our tables
				final byte[] keyBytes = SqrlUtil.base64UrlDecodeDataFromSqrlClient(entry.getValue());
				requestKeyTableRaw.put(keyType, keyBytes);
				requestKeyTableBase64.put(keyType, keyBase64);
			}
		}
		logger.debug(formatForLogging("keys found in request: {}", requestKeyTableBase64.keySet()));

		// Per the SQRL spec, since the server response is not signed, we must check the value that comes
		// back to ensure it wasn't tampered with
		final String expectedServerValue = persistence.fetchTransientAuthData(correlator,
				SqrlConstants.TRANSIENT_NAME_SERVER_PARROT);
		if (SqrlUtil.isBlank(expectedServerValue)) {
			throw new SqrlInvalidRequestException("Server parrot was not found in persistence");
		}
		if (!expectedServerValue.equals(serverParam)) {
			logger.warn(formatForLogging("Server parrot mismatch: Expected={}, Received={}", expectedServerValue,
					serverParam));
			throw new SqrlInvalidRequestException("Server parrot mismatch, possible tampering");
		}

		// Validate the signatures
		boolean idsFound = false;
		for (final SqrlSignatureType aSignatureType : SqrlSignatureType.values()) {
			final String signatureParamValue = servletRequest.getParameter(aSignatureType.toString());
			if (SqrlUtil.isNotBlank(signatureParamValue)) {
				// Validate the signature
				validateSignature(SqrlSignatureType.getSignatureToKeyParamTable().get(aSignatureType),
						signatureParamValue);
				if (aSignatureType == SqrlSignatureType.ids) {
					idsFound = true;
				}
			}
		}

		// All requests must have the ids signature
		if (!idsFound) {
			throw new SqrlInvalidRequestException(
					"ids was missing in SQRL client request: ", clientNameValuePairTable.toString());
		}

		final String clientCommandString = clientNameValuePairTable.get(SqrlClientParam.cmd.toString());
		try {
			this.clientCommand = SqrlRequestCommand.valueOf(clientCommandString.toUpperCase());
		} catch (final IllegalArgumentException e) {
			// We handle all SQRL v1 verbs, so don't set FUNCTIONS_NOT_SUPPORTED, treat it as an invalid
			// request instead
			throw new SqrlInvalidRequestException("Recevied invalid SQRL command from client: '" ,clientCommandString ,"'");
		}
	}

	/**
	 * The correlator is our only key to determining which user this is, so it's critical we parse this out first
	 */
	public static String parseCorrelatorOnly(final HttpServletRequest servletRequest) throws SqrlException {
		final String serverParam = getRequiredParameter(servletRequest, "server");
		// parse server - not a name value pair, just the query string we gave
		return extractFromSqrlCsvString(serverParam, SqrlClientParam.cor.toString());
	}

	private static String getRequiredParameter(final HttpServletRequest servletRequest, final String requiredParamName)
			throws SqrlInvalidRequestException {
		final String value = servletRequest.getParameter(requiredParamName);
		if (value == null || value.trim().length() == 0) {
			throw new SqrlInvalidRequestException("Missing required parameter " + requiredParamName
					+ ".  Request contained: " + SqrlUtil.buildRequestParamList(servletRequest));
		}
		try {
			SqrlSanitize.inspectIncomingData(value);
		} catch (final SqrlInvalidDataException e) {
			// Convert to SqrlInvalidRequestException since it came from a SQRL client app
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
		return value;
	}

	static String extractFromSqrlCsvString(final String serverParam, final String variableToFind)
			throws SqrlClientRequestProcessingException {
		final String toSearch = SqrlUtil.base64UrlDecodeDataFromSqrlClientToString(serverParam);
		String toFind = variableToFind;
		if (!variableToFind.endsWith("=")) {
			toFind += "=";
		}
		// Find the nut param
		int index = toSearch.indexOf(toFind);
		if (index == -1) {
			throw new SqrlInvalidRequestException("Could not find " + toFind + " in server param: " + toSearch);
		}
		String value = toSearch.substring(index + toFind.length());
		// Need to find the end of the nut string - could be & if from our login page URL or SqrlServerReply.SEPARATOR
		// if from a server reply
		index = value.indexOf(SqrlClientReply.SEPARATOR);
		if (index > -1) {
			value = value.substring(0, index);
		}
		index = value.indexOf('&');
		if (index > -1) {
			value = value.substring(0, index);
		}
		try {
			SqrlSanitize.inspectIncomingData(value);
		} catch (final SqrlInvalidDataException e) {
			// Convert to SqrlInvalidRequestException since it came from a SQRL client app
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
		return value;
	}

	private void validateSignature(final SqrlServerSideKey keyName, final String signatureParamValue)
			throws SqrlInvalidRequestException {
		final byte[] signatureFromMessage = SqrlUtil.base64UrlDecodeDataFromSqrlClient(signatureParamValue);

		try {
			final byte[] publicKey = requestKeyTableRaw.get(keyName);
			if (publicKey == null) {
				throw new SqrlInvalidRequestException(keyName.toString(), " not found in client param: ", clientParam);
			}
			final byte[] messageBytes = (clientParam + serverParam).getBytes();
			final boolean isSignatureValid = SqrlUtil.verifyED25519(signatureFromMessage, messageBytes, publicKey);
			if (!isSignatureValid) {
				throw new SqrlInvalidRequestException("Signature for ", keyName.toString(), " was invalid");
			}
		} catch (final SqrlInvalidRequestException e) {
			throw e;
		} catch (final Exception e) {
			throw new SqrlInvalidRequestException(e, "Error computing signature for ", keyName.toString());
		}
	}

	private Map<String, String> parseLinesToNameValueMap(final String decoded) throws SqrlInvalidRequestException {
		final Map<String, String> table = new TreeMap<>();
		final BufferedReader reader = new BufferedReader(new StringReader(decoded));
		try {
			String line = reader.readLine();
			while (line != null) {
				final String[] data = line.split("=");
				if (data.length != 2) {
					logger.info("Received empty param " + line);
				} else {
					table.put(data[0], data[1]);
				}
				line = reader.readLine();
			}
			return table;
		} catch (final Exception e) {
			throw new SqrlInvalidRequestException(e, "Exception parsing decoded <", decoded, ">");
		}
	}

	public SqrlRequestCommand getClientCommand() {
		return clientCommand;
	}

	public SqrlNutToken0 getNut() {
		return nut;
	}

	public Map<String, String> getKeysToBePersisted() {
		// Convert to type <String, String> to ease persistence call
		final Map<String, String> toBePersisted = new ConcurrentHashMap<>();
		for (final Map.Entry<SqrlServerSideKey, String> entry : requestKeyTableBase64.entrySet()) {
			if (entry.getKey().needsToBePersisted()) {
				toBePersisted.put(entry.getKey().toString(), entry.getValue());
			}
		}
		return toBePersisted;
	}

	public boolean hasKey(final SqrlServerSideKey key) {
		return requestKeyTableBase64.containsKey(key);
	}

	public String getKey(final SqrlServerSideKey key) {
		return requestKeyTableBase64.get(key);
	}

	public Set<SqrlRequestOpt> getOptList() {
		return optList;
	}

	public String getNegotiatedSqrlProtocolVersion() {
		return negotiatedSqrlProtocolVersion.toString();
	}

	public String getCorrelator() {
		return correlator;
	}

	/**
	 * @return true if the request contained a valid urs signature
	 */
	public boolean containsUrs() {
		return SqrlUtil.isNotBlank(servletRequest.getParameter(SqrlSignatureType.urs.toString()));
	}
} public abstract class SqrlNutToken0 {
	/**
	 * @return the encrypted, base64 representation of this token
	 */
	public abstract String asEncryptedBase64();

	abstract long getIssuedTimestampMillis();

	/**
	 * @return the InetAddress of device which originated the browser request. This can return null <b>if</b>
	 *         <code>compareSqrlClientInetAddress<code> is overridden
	 */
	abstract InetAddress getBrowserIPAddress();

	/**
	 * Default implementation to compute when this SQRL "nut" token will expire
	 *
	 * @param config
	 *            the SQRL config
	 * @return the time, in millis, when the token will expire
	 */
	public long computeExpiresAt(final SqrlConfig config) {
		final long nutValidityMillis = TimeUnit.SECONDS.toMillis(config.getNutValidityInSeconds());
		return getIssuedTimestampMillis() + nutValidityMillis;
	}

	/**
	 * Default implementation to perform a comparison of the IP address between the browser and the SQRL client
	 * 
	 * @param sqrlClientIpAddress
	 *            the IP address SQRL client which sent the backchannel request
	 * @return Optional.empty if the IP addresses matched, Optional.string if they did not including detail of the
	 *         mismatch for debugging
	 */
	public Optional<String> compareSqrlClientInetAddress(final InetAddress sqrlClientIpAddress, final SqrlConfig config)
			throws SqrlException {
		final InetAddress browserIpAddress = getBrowserIPAddress();
		if (sqrlClientIpAddress.equals(browserIpAddress)) {
			return Optional.empty();
		} else if ((sqrlClientIpAddress instanceof Inet4Address && browserIpAddress instanceof Inet6Address)
				|| (sqrlClientIpAddress instanceof Inet6Address && browserIpAddress instanceof Inet4Address)) {
			// One side saw an IPv4 while the other side saw IPv6, these will never match
			if (sqrlClientIpAddress.isLoopbackAddress() && browserIpAddress.isLoopbackAddress()) {
				return Optional.empty();
			}
			return Optional.of(SqrlUtil.buildString("IP address format mismatch, browserIP=",
					browserIpAddress.getHostAddress(), " sqrlClientIP=", sqrlClientIpAddress.getHostAddress()));
		} else {
			return Optional.of(SqrlUtil.buildString("IP address mismatch, browserIP=",
					browserIpAddress.getHostAddress(), " sqrlClientIP=", sqrlClientIpAddress.getHostAddress()));
		}
	}

	/**
	 * Helper method to convert translate from a byte to an unsigned byte represented by an int
	 */
	static int buildFormatId(final byte unsignedByte) {
		return Byte.toUnsignedInt(unsignedByte);
	}
} public class TestCaseSqrlNutHelper {
	public static SqrlNutToken0 buildValidSqrlNut(long timestamp, SqrlConfig config)
			throws SqrlException, UnknownHostException {
		final SqrlNutToken1SingleBlockFormat nut = new SqrlNutToken1SingleBlockFormat(getLocalHost(),
				TestCaseUtil.buildSqrlConfigOperations(config), timestamp);
		return nut;
	}

	public static SqrlNutToken1SingleBlockFormat buildValidSqrlNutTokenLegacyFormat(long timestamp, SqrlConfig config)
			throws SqrlException, UnknownHostException {
		final SqrlNutToken1SingleBlockFormat nut = new SqrlNutToken1SingleBlockFormat(getLocalHost(),
				TestCaseUtil.buildSqrlConfigOperations(config), timestamp);
		return nut;
	}

	public static InetAddress getLocalHost() throws UnknownHostException {
		return InetAddress.getLocalHost();
	}
} public enum SqrlRequestCommand {
	// @formatter:off
	QUERY,
	IDENT,
	DISABLE,
	ENABLE,
	REMOVE,
	// @formatter:on
	;

	/**
	 * The SQRL spec states that opt args should only be processed during certain commands
	 *
	 * @return
	 */
	public boolean shouldProcessOpts() {
		// opt args should only be processed during non-query per the spec
		// we also add remove since it doesn't make sense to process data we are deleting
		return this != QUERY && this != REMOVE;
	}
} public enum SqrlRequestOpt {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	/**
	 * “suk” is the abbreviation for Server Unlock Key. The presence of this flag instructs the SQRL server to return
	 * the stored server unlock key (SUK) associated with whichever identity matches the identity supplied by the SQRL
	 * client. The SQRL specification requires the SQRL server to automatically return the account's matching SUK
	 * whenever it is able to anticipate that the client is likely to require it, such as when the server contains a
	 * previous identity key, or when the account is disabled. However, there are instances where the client may know
	 * that it is going to need the stored SUK from the server, such as when it wishes to remove a non-disabled account.
	 * The client could first disable the account to induce the server to return the SUK, but it's simpler for the
	 * client to request the SUK from the server whenever it wants it. It's also conceivable that future extensions of
	 * this specification will incorporate other instances where the server's stored SUK is required for RescueCode
	 * based authentication.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	suk(false, true, false),

	/**
	 * When present, this option requests the web server to set a flag on this user's account to disable any alternative
	 * non-SQRL authentication capability, such as weaker traditional username and password authentication.
	 *
	 * Users who have become confident of their use of SQRL may ask their client to include this optional request. The
	 * web server should only assume this intention if the option is present in any successful non-query transaction.
	 * Its absence from any successful non-query transaction should immediately reset the flag and the prohibition in
	 * the web server. The web server may, at its option, notice when any change has occurred and explicitly ask the
	 * user to affirm their changed intention.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	sqrlonly(true, false, true),

	/**
	 * When present, this option requests the web server to set a flag on this user's account to disable any alternative
	 * “out of band” change to this user's SQRL identity, such as traditional and weak “what as your favorite pet's
	 * name” non-SQRL identity authentication.
	 *
	 * Users who have become confident of their use of SQRL may ask their client to include this optional request. The
	 * web server should only assume this intention if the option is present in any successful non-query transaction.
	 * Its absence from any successful non-query transaction should immediately reset the flag and the prohibition in
	 * the web server. The web server may, at its option, notice when any change has occurred and explicitly ask the
	 * user to affirm their changed intention.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	hardlock(true, false, true),

	/**
	 * “cps” is the abbreviation for Client Provided Session. The presence of this flag alters the system's final
	 * authentication action. In the traditional case when this flag is NOT present, the web server arranges to refresh
	 * the web browser's page or redirect the web browser to an authenticated (typically, logged-on) page. However, for
	 * increased security the “cps” option may be specified by a SQRL client to request the server to abandon its
	 * pending authentication with the user's browser and instead provide the web URL to the client for subsequent
	 * handling. The client then might present its user with a URL link to click, or it might arrange its own private
	 * communications channel to the users web browser so that the user will be taken to the identity-authenticated web
	 * page. This higher security mode has the advantage of completely defeating any known man-in-the-middle attack
	 * where an attacking third-party might have inserted themselves into the loop. This cuts that party out of the
	 * loop, preventing them from obtaining the user's authentication.
	 *
	 * The server provides the authenticated page URL in its “url={url}” response parameter for the final non-query
	 * command.
	 *
	 * from https://www.grc.com/sqrl/semantics.htm
	 */
	cps(true, false, false),
	
	/**
	 * this is needed to prevent split-authentication attacks from allowing 'query' 
	 * commands to succeed before perhaps intercepting a follow-up 'ident' command whose IPs do not match
	 * 
	 * if the server does not receive the "noiptest" option and the IP do not match, the operation 
	 * will be hard failed with the server's TIF flags having "0x40 Command failed" =SET= and 
	 * "0x04 IPs matched" =RESET=.  Since this is not a client failure, the 
	 * "0x80 Client failure" bit will not be set
	 *
	 * from https://www.grc.com/x/news.exe?cmd=article&group=grc.sqrl&item=18016&utag=
	 */
	noiptest(false, false, false),

	;

	private final boolean nonQueryOnly;
	private final boolean keyOpt;
	/**
	 * Some opts are user preferences and therefore need to be persisted in storage
	 */
	private final boolean persist;

	private SqrlRequestOpt(final boolean nonQueryOnly, final boolean keyOpt, boolean persist) {
		this.nonQueryOnly = nonQueryOnly;
		this.keyOpt = keyOpt;
		if (keyOpt) {
			this.persist = false;
		} else {
			this.persist = persist;
		}
	}

	/**
	 * Per the spec, some flags are to be processed during non-query calls only; account for that here
	 *
	 * @return
	 */
	public boolean isNonQueryOnly() {
		return nonQueryOnly;
	}

	public static Set<SqrlRequestOpt> getKeyOpts() {
		final Set<SqrlRequestOpt> keyOptSet = new HashSet<>();
		for(final SqrlRequestOpt opt : values()) {
			if (opt.keyOpt) {
				keyOptSet.add(opt);
			}
		}
		return keyOptSet;
	}

	/**
	 * Some opts are user preferences and therefore need to be persisted in storage
	 */
	public boolean isPersist() {
		return persist;
	}
} public enum SqrlServerSideKey {
	/*
	 * The spec defines these in lower case, so we do the same. It breaks java convention but is more readable 
	 */
	
	// @formatter:off
	idk(false),
	pidk(false),
	suk(true),
	vuk(true);

	// @formatter:on
	/**
	 * Whether the given key needs to be associated with a SQRL Identity and persisted by the SQRL server
	 */
	private final boolean needsBePersisted;

	SqrlServerSideKey(final boolean shouldBeStored) {
		this.needsBePersisted = shouldBeStored;
	}

	private static final Map<String, SqrlServerSideKey> LOOKUP_TABLE;
	static {
		final Map<String, SqrlServerSideKey> tempTable = new TreeMap<>();
		for (final SqrlServerSideKey aKey : values()) {
			tempTable.put(aKey.toString(), aKey);
		}
		LOOKUP_TABLE = Collections.unmodifiableMap(tempTable);
	}

	/**
	 * A version of {@code values} which is case-insensitive and returns null when not found instead of throwing an
	 * exception
	 *
	 * @return the key for this name or null
	 */
	public static SqrlServerSideKey valueOfOrNull(final String toFind) {
		return LOOKUP_TABLE.get(toFind.toLowerCase());
	}

	/**
	 * Whether the given key, if sent in a SQRL client request, must be associated with the users SQRL Identity and
	 * persisted
	 *
	 * @return if the key should be persisted
	 */
	public boolean needsToBePersisted() {
		return needsBePersisted;
	}

	/**
	 * Checks to see if a given name from the {@CLIENT} param is a key that needs to be associated with the users SQRL
	 * Identity and persisted
	 *
	 * @param dataName
	 *            name of a name/value pair passed in the {@CLIENT} param
	 * @return true if it represents a key and that key should be persisted
	 */
	public static boolean isRequestDataAKeyThatNeedsToBePersisted(final String dataName) {
		for (final SqrlServerSideKey aKey : SqrlServerSideKey.values()) {
			if (aKey.toString().equals(dataName) && SqrlServerSideKey.valueOf(dataName).needsToBePersisted()) {
				return true;
			}
		}
		return false;
	}
} public class SqrlAutoCloseablePersistence implements SqrlPersistence, AutoCloseable {
	private static final Logger		logger	= LoggerFactory.getLogger(SqrlAutoCloseablePersistence.class);
	private final SqrlPersistence	sqrlPersistence;

	public SqrlAutoCloseablePersistence(final SqrlPersistence sqrlPersistence) {
		this.sqrlPersistence = sqrlPersistence;
	}

	@Override
	public void close() {
		if (!sqrlPersistence.isClosed()) {
			// Assume an error occurred since SqrlPersistence.commit was not called
			logger.warn("Closing entity manager with rollback in error state");
			sqrlPersistence.closeRollback();
		}
	}

	/* ************** SqrlPersistence delegate methods generated by eclipse ***************/
	@Override
	public void createAndEnableSqrlIdentity(final String sqrlIdk) {
		sqrlPersistence.createAndEnableSqrlIdentity(sqrlIdk);
	}

	@Override
	public boolean doesSqrlIdentityExistByIdk(final String sqrlIdk) {
		return sqrlPersistence.doesSqrlIdentityExistByIdk(sqrlIdk);
	}

	@Override
	public SqrlIdentity fetchSqrlIdentityByUserXref(final String appUserXref) {
		return sqrlPersistence.fetchSqrlIdentityByUserXref(appUserXref);
	}

	@Override
	public void updateIdkForSqrlIdentity(final String previousSqrlIdk, final String newSqrlIdk) {
		sqrlPersistence.updateIdkForSqrlIdentity(previousSqrlIdk, newSqrlIdk);
	}

	@Override
	public void deleteSqrlIdentity(final String sqrlIdk) {
		sqrlPersistence.deleteSqrlIdentity(sqrlIdk);
	}

	@Override
	public void updateNativeUserXref(final long sqrlIdentityId, final String nativeUserXref) {
		sqrlPersistence.updateNativeUserXref(sqrlIdentityId, nativeUserXref);
	}

	@Override
	public void userAuthenticatedViaSqrl(final String sqrlIdk, final String correlator) {
		sqrlPersistence.userAuthenticatedViaSqrl(sqrlIdk, correlator);
	}

	@Override
	public boolean fetchSqrlFlagForIdentity(final String sqrlIdk, final SqrlIdentityFlag flagToFetch) {
		return sqrlPersistence.fetchSqrlFlagForIdentity(sqrlIdk, flagToFetch);
	}

	@Override
	public void setSqrlFlagForIdentity(final String sqrlIdk, final SqrlIdentityFlag flagToSet, final boolean valueToSet) {
		sqrlPersistence.setSqrlFlagForIdentity(sqrlIdk, flagToSet, valueToSet);
	}

	@Override
	public void storeSqrlDataForSqrlIdentity(final String sqrlIdk, final Map<String, String> dataToStore) {
		sqrlPersistence.storeSqrlDataForSqrlIdentity(sqrlIdk, dataToStore);
	}

	@Override
	public String fetchSqrlIdentityDataItem(final String sqrlIdk, final String toFetch) {
		return sqrlPersistence.fetchSqrlIdentityDataItem(sqrlIdk, toFetch);
	}

	@Override
	public boolean hasTokenBeenUsed(final String nutTokenString) {
		return sqrlPersistence.hasTokenBeenUsed(nutTokenString);
	}

	@Override
	public void markTokenAsUsed(final String nutTokenString, final Date expiryTime) {
		sqrlPersistence.markTokenAsUsed(nutTokenString, expiryTime);
	}

	@Override
	public String fetchTransientAuthData(final String correlator, final String transientNameServerParrot) {
		return sqrlPersistence.fetchTransientAuthData(correlator, transientNameServerParrot);
	}

	@Override
	public SqrlCorrelator createCorrelator(final String correlatorString, final Date expiryTime) {
		return sqrlPersistence.createCorrelator(correlatorString, expiryTime);
	}

	@Override
	public SqrlCorrelator fetchSqrlCorrelator(final String correlator) {
		return sqrlPersistence.fetchSqrlCorrelator(correlator);
	}

	@Override
	public SqrlCorrelator fetchSqrlCorrelatorRequired(final String correlator) {
		return sqrlPersistence.fetchSqrlCorrelatorRequired(correlator);
	}

	@Override
	public void closeCommit() {
		sqrlPersistence.closeCommit();
	}

	@Override
	public void closeRollback() {
		sqrlPersistence.closeRollback();
	}

	@Override
	public boolean isClosed() {
		return sqrlPersistence.isClosed();
	}

	@Override
	public void cleanUpExpiredEntries() {
		sqrlPersistence.cleanUpExpiredEntries();
	}

	@Override
	public Map<String, SqrlCorrelator> fetchSqrlCorrelatorsDetached(final Set<String> correlatorStringSet) {
		return sqrlPersistence.fetchSqrlCorrelatorsDetached(correlatorStringSet);
	}

	@Override
	public Map<String, SqrlAuthenticationStatus> fetchSqrlCorrelatorStatusUpdates(
			final Map<String, SqrlAuthenticationStatus> correlatorToCurrentStatusTable) {
		return sqrlPersistence.fetchSqrlCorrelatorStatusUpdates(correlatorToCurrentStatusTable);
	}

	@Override
	public void deleteSqrlCorrelator(final SqrlCorrelator sqrlCorrelator) {
		sqrlPersistence.deleteSqrlCorrelator(sqrlCorrelator);
	}
} @Entity
@Table(name = "sqrl_correlator")
public class SqrlCorrelator implements Serializable {
	private static final long serialVersionUID = -670589151677266808L;

	@Id
	@TableGenerator(name = "correlator_gen", table = "sqrl_db_id_gen", pkColumnName = "name", valueColumnName = "value",
	allocationSize = 1)
	@GeneratedValue(generator = "correlator_gen")
	@Column(name = "id")
	private long id;

	@Column(name = "value", nullable = false)
	private String value;

	@Column(name = "authenticationStatus", nullable = false)
	@Enumerated(EnumType.STRING)
	SqrlAuthenticationStatus authenticationStatus = SqrlAuthenticationStatus.CORRELATOR_ISSUED;

	@Column(name = "expiryTime", nullable = false)
	@Temporal(TemporalType.TIMESTAMP)
	private Date expiryTime;

	@ElementCollection(fetch = FetchType.EAGER)
	@CollectionTable(name = "sqrl_transient_auth_data",
	joinColumns = @JoinColumn(name = "id", referencedColumnName = "id"))
	@MapKeyColumn(name = "name")
	@Column(name = "value", length = 5000)
	private final Map<String, String> transientAuthDataTable = new HashMap<>();

	@ManyToOne
	@JoinColumn(name = "authenticated_identity", nullable = true)
	private SqrlIdentity authenticatedIdentity;

	public SqrlCorrelator() {
		// Required by JPA
	}

	public SqrlCorrelator(final String correlatorString, final Date expiryTime) {
		this.value = correlatorString;
		this.expiryTime = expiryTime;
	}

	public Date getExpiryTime() {
		return expiryTime;
	}

	public void setExpiryTime(final Date expiryTime) {
		this.expiryTime = expiryTime;
	}

	public Map<String, String> getTransientAuthDataTable() {
		return transientAuthDataTable;
	}

	public SqrlIdentity getAuthenticatedIdentity() {
		if (getAuthenticationStatus().isAuthComplete() == false) {
			throw new SqrlPersistenceException( // TOOD: create SqrlRuntimeException
					"getAuthenticatedIdentity() can only be called when getAuthenticationStatus() == SqrlAuthenticationStatus.AUTH_COMPLETE");
		}
		return authenticatedIdentity;
	}

	public void setAuthenticationStatus(final SqrlAuthenticationStatus authenticationStatus) {
		this.authenticationStatus = authenticationStatus;
	}

	public SqrlAuthenticationStatus getAuthenticationStatus() {
		return authenticationStatus;
	}

	public String getCorrelatorString() {
		return value;
	}

	public void setAuthenticatedIdentity(final SqrlIdentity authenticatedIdentity) {
		this.authenticatedIdentity = authenticatedIdentity;
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("SqrlCorrelator [id=");
		builder.append(id);
		builder.append(", value=");
		builder.append(value);
		builder.append(", authenticationStatus=");
		builder.append(authenticationStatus);
		builder.append(", expiryTime=");
		builder.append(expiryTime);
		builder.append(", transientAuthDataTable=");
		builder.append(transientAuthDataTable);
		builder.append(", authenticatedIdentity=");
		builder.append(authenticatedIdentity);
		builder.append("]");
		return builder.toString();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((authenticatedIdentity == null) ? 0 : authenticatedIdentity.hashCode());
		result = prime * result + ((authenticationStatus == null) ? 0 : authenticationStatus.hashCode());
		result = prime * result + ((expiryTime == null) ? 0 : expiryTime.hashCode());
		result = prime * result + (int) (id ^ (id >>> 32));
		result = prime * result + ((transientAuthDataTable == null) ? 0 : transientAuthDataTable.hashCode());
		result = prime * result + ((value == null) ? 0 : value.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final SqrlCorrelator other = (SqrlCorrelator) obj;
		if (authenticatedIdentity == null) {
			if (other.authenticatedIdentity != null) {
				return false;
			}
		} else if (!authenticatedIdentity.equals(other.authenticatedIdentity)) {
			return false;
		}
		if (authenticationStatus != other.authenticationStatus) {
			return false;
		}
		if (expiryTime == null) {
			if (other.expiryTime != null) {
				return false;
			}
		} else if (!expiryTime.equals(other.expiryTime)) {
			return false;
		}
		if (id != other.id) {
			return false;
		}
		if (transientAuthDataTable == null) {
			if (other.transientAuthDataTable != null) {
				return false;
			}
		} else if (!transientAuthDataTable.equals(other.transientAuthDataTable)) {
			return false;
		}
		if (value == null) {
			if (other.value != null) {
				return false;
			}
		} else if (!value.equals(other.value)) {
			return false;
		}
		return true;
	}

	public long getId() {
		return id;
	}

} public class SqrlJpaPersistenceProvider implements SqrlPersistence {
	private static final Logger logger = LoggerFactory.getLogger(SqrlJpaPersistenceProvider.class);

	public static final String	PERSISTENCE_UNIT_NAME	= "javasqrl-persistence";
	private static final String	PARAM_CORRELATOR		= "correlator";

	private static final Map<EntityManager, Long>	LAST_USED_TIME_TABLE	= new WeakHashMap<>();
	// Need strong references so we can check that it was closed, will be removed below
	private static final Map<EntityManager, Exception> CREATED_BY_STACK_TABLE = new ConcurrentHashMap<>();

	private static EntityManagerFactory					entityManagerFactory;
	private final EntityManager							entityManager;

	/**
	 * @deprecated do not invoke this constructor directly, use <code>SqrlJpaPersistenceFactory</code> instead
	 */
	@Deprecated
	public SqrlJpaPersistenceProvider() {
		entityManagerFactory = Persistence
				.createEntityManagerFactory(SqrlJpaPersistenceProvider.PERSISTENCE_UNIT_NAME);
		entityManager = entityManagerFactory.createEntityManager();
		entityManager.getTransaction().begin();
		LAST_USED_TIME_TABLE.put(entityManager, System.currentTimeMillis());
		CREATED_BY_STACK_TABLE.put(entityManager, new Exception("create SqrlJpaPersistenceProvider trace"));
	}
	
	private static void checkInit() {
		if (entityManagerFactory == null) {
			entityManagerFactory = Persistence
					.createEntityManagerFactory(SqrlJpaPersistenceProvider.PERSISTENCE_UNIT_NAME);
		}
	}

	private void updateLastUsed(final EntityManager entityManger) {
		LAST_USED_TIME_TABLE.put(entityManger, System.currentTimeMillis());
	}

	@Override
	public boolean doesSqrlIdentityExistByIdk(final String sqrlIdk) {
		updateLastUsed(entityManager);
		return fetchSqrlIdentity(sqrlIdk) != null;
	}

	@Override
	public void updateIdkForSqrlIdentity(final String previousSqrlIdk, final String newSqrlIdk) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = fetchRequiredSqrlIdentity(previousSqrlIdk);
		sqrlIdentity.setIdk(newSqrlIdk);
	}

	private SqrlIdentity fetchSqrlIdentity(final String sqrlIdk) {
		updateLastUsed(entityManager);
		return (SqrlIdentity) returnOneOrNull(
				entityManager.createQuery("SELECT i FROM SqrlIdentity i WHERE i.idk = :sqrlIdk")
				.setParameter("sqrlIdk", sqrlIdk).getResultList());
	}

	private SqrlIdentity fetchRequiredSqrlIdentity(final String sqrlIdk) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = fetchSqrlIdentity(sqrlIdk);
		if (sqrlIdentity == null) {
			throw new SqrlPersistenceException("SqrlIdentity does not exist for idk=" + sqrlIdk);
		} else {
			return sqrlIdentity;
		}
	}

	@Override
	public SqrlIdentity fetchSqrlIdentityByUserXref(final String userXref) {
		updateLastUsed(entityManager);
		return (SqrlIdentity) returnOneOrNull(
				entityManager.createQuery("SELECT i FROM SqrlIdentity i WHERE i.nativeUserXref = :userXref")
				.setParameter("userXref", userXref).getResultList());
	}

	@Override
	public void deleteSqrlIdentity(final String sqrlIdk) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = fetchSqrlIdentity(sqrlIdk);
		if (sqrlIdentity == null) {
			logger.warn("Can't find idk " + sqrlIdk + " to delete");
		} else {
			entityManager.remove(sqrlIdentity);
		}
	}

	@Override
	public void userAuthenticatedViaSqrl(final String sqrlIdk, final String correlatorString) {
		updateLastUsed(entityManager);
		// Find the sqrlIdentity and mark SQRL authentication as occurred
		final SqrlCorrelator sqrlCorrelator = fetchSqrlCorrelatorRequired(correlatorString);
		final SqrlIdentity sqrlIdentity = fetchRequiredSqrlIdentity(sqrlIdk);
		sqrlCorrelator.setAuthenticatedIdentity(sqrlIdentity);
		if (sqrlCorrelator.getAuthenticationStatus() != AUTHENTICATED_CPS) {
			sqrlCorrelator.setAuthenticationStatus(AUTHENTICATED_BROWSER);
		}
	}

	@Override
	public void updateNativeUserXref(final long sqrlIdentityDbId, final String nativeUserXref) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = entityManager.find(SqrlIdentity.class, sqrlIdentityDbId);
		sqrlIdentity.setNativeUserXref(nativeUserXref);
	}

	/* ************************ Sqrl Correlator methods *****************************/

	@Override
	public SqrlCorrelator fetchSqrlCorrelator(final String sqrlCorrelatorString) {
		updateLastUsed(entityManager);
		return (SqrlCorrelator) returnOneOrNull(
				entityManager.createQuery("SELECT i FROM SqrlCorrelator i WHERE i.value = :correlator")
				.setParameter(PARAM_CORRELATOR, sqrlCorrelatorString).getResultList());
	}

	@Override
	public SqrlCorrelator fetchSqrlCorrelatorRequired(final String sqrlCorrelatorString) {
		updateLastUsed(entityManager);
		final SqrlCorrelator sqrlCorrelator = fetchSqrlCorrelator(sqrlCorrelatorString);
		if (sqrlCorrelator == null) {
			throw new SqrlPersistenceException("SqrlCorrelator does not exist for correlator=" + sqrlCorrelatorString);
		} else {
			return sqrlCorrelator;
		}
	}

	@Override
	public Map<String, SqrlCorrelator> fetchSqrlCorrelatorsDetached(final Set<String> correlatorStringSet) {
		updateLastUsed(entityManager);
		if (correlatorStringSet.isEmpty()) {
			return Collections.emptyMap();
		}
		final StringBuilder buf = new StringBuilder("SELECT i FROM SqrlCorrelator i WHERE ");

		for (int i = 0; i < correlatorStringSet.size(); i++) {
			buf.append(" i.value = :correlator").append(i).append(" OR");
		}
		buf.replace(buf.length() - 3, buf.length(), ""); // Remove OR
		final TypedQuery<SqrlCorrelator> query = entityManager.createQuery(buf.toString(), SqrlCorrelator.class);
		int i = 0;
		for (final String correlatorString : correlatorStringSet) {
			query.setParameter(PARAM_CORRELATOR + (i++), correlatorString);
		}

		// Parse the result into a table
		final Map<String, SqrlCorrelator> resultTable = new ConcurrentHashMap<>();
		for (final SqrlCorrelator correlator : query.getResultList()) {
			entityManager.detach(correlator);
			resultTable.put(correlator.getCorrelatorString(), correlator);
		}
		return resultTable;
	}

	@Override
	public Map<String, SqrlAuthenticationStatus> fetchSqrlCorrelatorStatusUpdates(
			final Map<String, SqrlAuthenticationStatus> correlatorToCurrentStatusTable) {
		updateLastUsed(entityManager);
		if (correlatorToCurrentStatusTable.isEmpty()) {
			return Collections.emptyMap();
		}
		final StringBuilder buf = new StringBuilder("SELECT i FROM SqrlCorrelator i WHERE ");

		int counter = 0;
		for (final Map.Entry<String, SqrlAuthenticationStatus> entry : correlatorToCurrentStatusTable.entrySet()) {
			buf.append(" (i.value = :correlator").append(counter);
			// If we are in the end state (isAuthComplete), always return the result
			if (entry.getValue().isAuthComplete() == false) {
				buf.append(" AND i.authenticationStatus <> :authenticationStatus").append(counter);
			}
			buf.append(" ) OR");
			counter++;
		}
		buf.replace(buf.length() - 2, buf.length(), ""); // Remove "OR"
		final TypedQuery<SqrlCorrelator> query = entityManager.createQuery(buf.toString(), SqrlCorrelator.class);
		counter = 0;
		final StringBuilder debugBuf = new StringBuilder(buf);
		for (final Map.Entry<String, SqrlAuthenticationStatus> entry : correlatorToCurrentStatusTable.entrySet()) {
			query.setParameter(PARAM_CORRELATOR + counter, entry.getKey());
			updateDebugBuf(debugBuf, ":correlator" + counter, entry.getKey());
			if (entry.getValue().isAuthComplete() == false) {
				query.setParameter("authenticationStatus" + counter, entry.getValue());
				updateDebugBuf(debugBuf, ":authenticationStatus" + counter, entry.getValue().toString());
			}
			counter++;
		}

		logger.trace("monitor correaltor for change SQL: {}", debugBuf);
		// Parse the result into a table
		final Map<String, SqrlAuthenticationStatus> resultTable = new ConcurrentHashMap<>();
		for (final SqrlCorrelator correlator : query.getResultList()) {
			resultTable.put(correlator.getCorrelatorString(), correlator.getAuthenticationStatus());
		}
		return resultTable;
	}

	private static void updateDebugBuf(final StringBuilder buf, final String name, final String value) {
		final int start = buf.indexOf(name);
		final int end = start + name.length();
		buf.replace(start, end, "'" + value + "'");
	}

	@Override
	public void storeSqrlDataForSqrlIdentity(final String sqrlIdk, final Map<String, String> dataToStore) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = fetchSqrlIdentity(sqrlIdk);
		if (sqrlIdentity == null) {
			throw new SqrlPersistenceException("SqrlIdentity not found for " + sqrlIdk);
		}
		storeSqrlDataForSqrlIdentity(sqrlIdentity, dataToStore);
	}

	private void storeSqrlDataForSqrlIdentity(final SqrlIdentity sqrlIdentity, final Map<String, String> dataToStore) {
		updateLastUsed(entityManager);
		// Update any SQRL specific data we have received from the SQRL client
		if (!dataToStore.isEmpty()) {
			sqrlIdentity.getIdentityDataTable().putAll(dataToStore);
		}
		entityManager.persist(sqrlIdentity);
	}

	@Override
	public String fetchSqrlIdentityDataItem(final String sqrlIdk, final String toFetch) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = fetchSqrlIdentity(sqrlIdk);
		if (sqrlIdentity == null) {
			throw new SqrlPersistenceException("Couldn't find SqrlIdentity for idk " + sqrlIdk);
		} else {
			return sqrlIdentity.getIdentityDataTable().get(toFetch);
		}
	}

	@Override
	public boolean hasTokenBeenUsed(final String nutTokenString) {
		updateLastUsed(entityManager);
		return entityManager.find(SqrlUsedNutToken.class, nutTokenString) != null;
	}

	@Override
	public void markTokenAsUsed(final String nutTokenString, final Date expiryTime) {
		updateLastUsed(entityManager);
		final SqrlUsedNutToken sqrlUsedNutToken = new SqrlUsedNutToken(nutTokenString, expiryTime);
		entityManager.persist(sqrlUsedNutToken);
	}

	@Override
	public String fetchTransientAuthData(final String correlator, final String dataName) {
		updateLastUsed(entityManager);
		final SqrlCorrelator correlatorObject = fetchSqrlCorrelatorRequired(correlator);
		return correlatorObject.getTransientAuthDataTable().get(dataName);
	}

	private Object returnOneOrNull(@SuppressWarnings("rawtypes") final List resultList) {
		if (resultList == null || resultList.isEmpty()) {
			return null;
		} else if (resultList.size() == 1) {
			return resultList.get(0);
		} else {
			throw new SqrlPersistenceException("Expected one, but found multiple results: " + resultList);
		}
	}

	@Override
	public void closeCommit() {
		closeServletTransaction(true);
	}

	@Override
	public void closeRollback() {
		closeServletTransaction(false);
	}

	private void closeServletTransaction(final boolean commit) {
		if (!entityManager.isOpen()) {
			throw new SqrlPersistenceException("EntityManager is not open");
		}
		if (commit) {
			entityManager.getTransaction().commit();
		} else {
			entityManager.getTransaction().rollback();
		}
		entityManager.close();
	}

	@Override
	public boolean fetchSqrlFlagForIdentity(final String sqrlIdk, final SqrlIdentityFlag flagToFetch) {
		return fetchRequiredSqrlIdentity(sqrlIdk).getEnabledFlagList().contains(flagToFetch);
	}

	@Override
	public void setSqrlFlagForIdentity(final String sqrlIdk, final SqrlIdentityFlag flagToSet, final boolean enableOrDisable) {
		final SqrlIdentity sqrlIdentity = fetchRequiredSqrlIdentity(sqrlIdk);
		final Set<SqrlIdentityFlag> enabledFlagSet = sqrlIdentity.getEnabledFlagList();
		if (enableOrDisable && !enabledFlagSet.add(flagToSet)) {
			logger.warn("INFO-ONLY-STACK: Attempt to enable flag " + flagToSet + " that is already present",
					new SqrlDebugException());
			return; // Don't call entityManager.persist(sqrlIdentity);
		} else if (!enabledFlagSet.remove(flagToSet)) {
			logger.warn("INFO-ONLY-STACK: Attempt to remove flag " + flagToSet + " that wasn't present",
					new SqrlDebugException());
			return; // Don't call entityManager.persist(sqrlIdentity);
		}
		entityManager.persist(sqrlIdentity);
	}

	@Override
	public void createAndEnableSqrlIdentity(final String sqrlIdk) {
		updateLastUsed(entityManager);
		final SqrlIdentity sqrlIdentity = new SqrlIdentity(sqrlIdk);
		sqrlIdentity.getEnabledFlagList().add(SqrlIdentityFlag.SQRL_AUTH_ENABLED);
		entityManager.persist(sqrlIdentity);
	}

	@Override
	public SqrlCorrelator createCorrelator(final String correlatorString, final Date expiryTime) {
		final SqrlCorrelator sqrlCorrelator = new SqrlCorrelator(correlatorString, expiryTime);
		entityManager.persist(sqrlCorrelator);
		return sqrlCorrelator;
	}

	@Override
	public void deleteSqrlCorrelator(final SqrlCorrelator sqrlCorrelator) {
		if (sqrlCorrelator == null) {
			return;
		}
		SqrlCorrelator toRemove = sqrlCorrelator;
		updateLastUsed(entityManager);
		if (!entityManager.contains(sqrlCorrelator)) {
			toRemove = fetchSqrlCorrelator(sqrlCorrelator.getCorrelatorString());
		}
		if (toRemove == null) {
			logger.debug("INFO-ONLY-STACK: Attempt to remove correlator that doesn't exist", new SqrlDebugException());
		} else {
			entityManager.remove(toRemove);
		}
	}

	@Override
	public boolean isClosed() {
		return !entityManager.isOpen();
	}

	@Override
	public void cleanUpExpiredEntries() {
		final Date now = new Date();
		int rowsDeleted = entityManager.createQuery("DELETE FROM SqrlCorrelator i WHERE i.expiryTime < :now")
				.setParameter("now", now, TemporalType.TIMESTAMP).executeUpdate();
		if (rowsDeleted > 0) {
			logger.info("SqrlCorrelatorc cleanup deleted {} rows", rowsDeleted);
		}

		rowsDeleted = entityManager.createQuery("DELETE FROM SqrlUsedNutToken i WHERE i.expiryTime < :now")
				.setParameter("now", now, TemporalType.TIMESTAMP).executeUpdate();
		if (rowsDeleted > 0) {
			logger.info("SqrlUsedNutToken cleanup deleted {} rows", rowsDeleted);
		}
	}

	/**
	 * A task which periodically checks the state of various {@link EntityManager} instances to ensure they are being
	 * closed properly by the library
	 *
	 * @author Dave Badia
	 *
	 */
	public static final class SqrlJpaEntityManagerMonitorTask implements Runnable {

		public SqrlJpaEntityManagerMonitorTask() {
			// As required by
		}

		public long computeThresholdInMillis() {
			if (logger.isDebugEnabled()) {
				return TimeUnit.MINUTES.toMillis(5);
			} else {
				return TimeUnit.SECONDS.toMillis(10);
			}
		}

		private void removeFromTrackingTable(final Iterator<EntityManager> iter, final EntityManager entityManager) {
			iter.remove();
			LAST_USED_TIME_TABLE.remove(entityManager); // May or may not exist, ok
		}

		@Override
		public void run() {
			try {
				final long threshold = computeThresholdInMillis();
				logger.trace("Running EntityManagerMonitorTimerTask");
				final Iterator<EntityManager> iter = CREATED_BY_STACK_TABLE.keySet().iterator();
				while (iter.hasNext()) {
					final EntityManager entityManager = iter.next();
					if (!entityManager.isOpen()) {
						logger.trace("entityManager closed, removing from monitor table");
						removeFromTrackingTable(iter, entityManager);
					} else {
						final Long lastUsed = LAST_USED_TIME_TABLE.get(entityManager);
						if (lastUsed == null) {
							logger.error(
									"EntityManagerMonitorTask found null lastUsedTime for entityManager which was created at",
									CREATED_BY_STACK_TABLE.get(entityManager));
						} else {
							if (System.currentTimeMillis() - lastUsed > threshold) {
								logger.error("Entity Manager is still open and has not been used for "
										+ threshold + "ms.  Closing now.  Was created from",
										CREATED_BY_STACK_TABLE.get(entityManager));
								entityManager.close();
								removeFromTrackingTable(iter, entityManager);
							}
						}
					}
				}
			} catch (final RuntimeException e) {
				// thread
				logger.error("Error running entity manager monitor check", e);
			}
		}
	}

} public class SqrlConstants {
	public static final Integer SQRL_VERSION_1 = Integer.valueOf(1);

	/**
	 * The SQRL versions which this library supports, stored in descending order. This is an ugly one liner, but we need
	 * to ensure it is sorted descending and unmodifiable
	 */
	// @formatter:off
	public static final Set<Integer> SUPPORTED_SQRL_VERSIONS = Collections.unmodifiableSet(
			new TreeSet<>(Arrays.asList(new Integer[] {
					SqrlConstants.SQRL_VERSION_1
			})).descendingSet());
	// @formatter:off

	public static final Charset UTF8_CHARSET = StandardCharsets.UTF_8;
	public static final String	SCHEME_HTTP			= "http";
	public static final String	SCHEME_HTTP_COLON	= SCHEME_HTTP + ":";
	public static final String	SCHEME_HTTPS		= "https";
	public static final String	SCHEME_HTTPS_COLON	= SCHEME_HTTPS + ":";
	public static final String	SCHEME_SQRL			= "sqrl";
	public static final String	FORWARD_SLASH		= "/";

	public static final CharSequence	FORWARD_SLASH_X2_127_0_0_1	= "//127.0.0.1";
	public static final int				AES_KEY_LENGTH				= 16;

	// TODO: make enums
	public static final String			TRANSIENT_NAME_SERVER_PARROT		= "lastServerParam";
	public static final String			TRANSIENT_ENTRY_URL					= "entryPointUrl";
	public static final String			TRANSIENT_CPS_NONCE					= "cpsNonce";
	public static final String			ERROR								= "error";

	public static final String	REGEX_BASE64_URL	= "[a-zA-Z0-9_-]+";
	/**
	 * Token being any request param, correlator, nut token, etc
	 */
	public static final int		MAX_SQRL_TOKEN_SIZE	= 30000;

	public static final String SQRL_ATOMOSPHERE_LIB_UPDATER_CLASS = "com.github.sqrlserverjava.atmosphere.AtmosphereClientAuthStateUpdater";

	private SqrlConstants() {
		// Constants class
	}
} @WebListener
public class SqrlServiceExecutor implements ServletContextListener {
	private static final Logger logger = LoggerFactory.getLogger(SqrlServiceExecutor.class);

	/**
	 * DB cleanup may be slow running, so ensure another thread is always available to check for status updates from
	 * SQRL clients
	 */
	private static final int						THREAD_COUNT		= 2;
	private static final ScheduledExecutorService	EXECUTOR_SERVICE	= Executors.newScheduledThreadPool(THREAD_COUNT,
			new SqrlThreadFactory());

	@SuppressWarnings("rawtypes")
	private static List<ScheduledFuture> backgroundTaskList = new ArrayList<>();

	@Override
	public void contextInitialized(final ServletContextEvent servletContextEvent) {
		// Perform dependency injection
		SqrlServerOperations.setExecutor(this);
		SqrlConfigOperations.setExecutor(this);
	}

	public void scheduleAtFixedRate(final Runnable runnable, final long initialDelay, final long period,
			final TimeUnit unit) {
		@SuppressWarnings("rawtypes")
		final ScheduledFuture future = EXECUTOR_SERVICE.scheduleAtFixedRate(runnable, initialDelay, period, unit);
		backgroundTaskList.add(future);
	}

	@Override
	public void contextDestroyed(final ServletContextEvent servletContextEvent) {
		logger.info("contextDestroyed invoked");
		shutdown();
	}
	
	public void shutdown() {
		logger.info("Shutting down background tasks and executor service");
		// @formatter:off
		for (@SuppressWarnings("rawtypes") final ScheduledFuture backgroundTask : backgroundTaskList) {
			// @formatter:on
			backgroundTask.cancel(false);
		}
		EXECUTOR_SERVICE.shutdown();
	}

	/**
	 * The only reason for this is to give our threads meaningful names
	 *
	 * @author Dave Badia
	 *
	 */
	private static class SqrlThreadFactory implements ThreadFactory {
		private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(1);

		@Override
		public Thread newThread(final Runnable r) {
			return new Thread(r, "Sqrl Background #" + THREAD_COUNTER.getAndIncrement());
		}
	}
} public class SqrlUtil {
	private static final Logger					logger				= LoggerFactory.getLogger(SqrlUtil.class);
	private static final Map<String, String>	cookieDomainCache	= new ConcurrentHashMap<>();
	private static final List<String> LOCALHOST_IP_STRING_LIST = Collections
			.unmodifiableList(Arrays.asList("127.0.0.1", "0:0:0:0:0:0:0:1"));
	static final Pattern				REGEX_PATTERN_REGEX_BASE64_URL	= Pattern
			.compile(SqrlConstants.REGEX_BASE64_URL);

	private SqrlUtil() {
		// Util class
	}

	/**
	 * Performs the SQRL required base64URL encoding (base64url without padding)
	 *
	 * @param bytes
	 *            the data to be encoded
	 * @return the encoded string
	 */
	public static String sqrlBase64UrlEncode(final byte[] bytes) {
		String encoded = new String(Base64.getUrlEncoder().encode(bytes), SqrlConstants.UTF8_CHARSET);
		while (encoded.endsWith("=")) {
			encoded = encoded.substring(0, encoded.length() - 1);
		}
		return encoded;
	}

	/**
	 * Performs the SQRL required base64URL encoding (base64url without padding)
	 *
	 * @param toEncode
	 *            the string to be encoded
	 * @return the encoded string
	 */
	public static String sqrlBase64UrlEncode(final String toEncode) { // TODO: rename to base64UrlEncode
		return sqrlBase64UrlEncode(toEncode.getBytes(SqrlConstants.UTF8_CHARSET));
	}

	/**
	 * Performs the SQRL required base64URL decoding (base64url without padding)
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded byte array
	 * @throws SqrlException
	 *             if an error occurs during the base64 decode
	 */
	public static byte[] base64UrlDecode(final String toDecodeParam) throws SqrlException {
		try {
			return Base64.getUrlDecoder().decode(toDecodeParam.getBytes());
		} catch (final IllegalArgumentException e) {
			throw new SqrlException(e, "Error base64 decoding: " + toDecodeParam);
		}
	}

	/**
	 * Convenience method which performs the SQRL required base64URL decoding but throws SqrlInvalidRequestException
	 * since the assumption is that the data originated from a SQRL client
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded byte array
	 * @throws SqrlInvalidRequestException
	 *             if an error occurs during the base64 decode
	 */
	public static byte[] base64UrlDecodeDataFromSqrlClient(final String toDecodeParam) throws SqrlInvalidRequestException {
		try {
			return base64UrlDecode(toDecodeParam);
		} catch (final SqrlException e) {
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
	}

	/**
	 * Performs the SQRL required base64URL decoding
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded data as a string using the UTF-8 character set
	 * @throws SqrlException
	 *             if UTF8 is not supported
	 */
	public static String base64UrlDecodeToString(final String toDecode) throws SqrlException {
		return new String(base64UrlDecode(toDecode), SqrlConstants.UTF8_CHARSET);
	}

	/**
	 * Convenience method which performs the SQRL required base64URL decoding but throws SqrlInvalidRequestException
	 * since the assumption is that the data originated from a SQRL client
	 *
	 * @param toDecodeParam
	 *            the data to be decoded
	 * @return the decoded data as a string using the UTF-8 character set
	 * @throws SqrlInvalidRequestException
	 *             if the data was not in base64url format
	 * @throws IllegalStateException
	 *             if UTF8 is not supported
	 */
	public static String base64UrlDecodeDataFromSqrlClientToString(final String toDecode)
			throws SqrlInvalidRequestException {
		try {
			return base64UrlDecodeToString(toDecode);
		} catch (final SqrlException e) {
			throw new SqrlInvalidRequestException(e, e.getMessage());
		}
	}



	public static String base64UrlDecodeToStringOrErrorMessage(final String toDecode) {
		try {
			return base64UrlDecodeToString(toDecode);
		} catch (final Exception e) {
			logger.error("Error during url decode, returning error string for " + toDecode, e);
			return "<error during base64url decode>";
		}
	}

	/**
	 * Internal use only. Verifies the ED25519 signature
	 *
	 * @param signatureFromMessage
	 *            the signature data
	 * @param messageBytes
	 *            the message that was signed
	 * @param publicKeyBytes
	 *            the public key to be used for verification
	 * @return true if verification was successful
	 * @throws SqrlException
	 *             if an error occurs during ED25519 operations
	 */
	public static boolean verifyED25519(final byte[] signatureFromMessage, final byte[] messageBytes,
			final byte[] publicKeyBytes) throws SqrlException {
		try {
			final Signature signature = new EdDSAEngine(MessageDigest.getInstance("SHA-512"));
			final EdDSAParameterSpec edDsaSpec = EdDSANamedCurveTable
					.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);

			final PublicKey publicKey = new EdDSAPublicKey(new EdDSAPublicKeySpec(publicKeyBytes, edDsaSpec));
			signature.initVerify(publicKey);

			signature.update(messageBytes);
			return signature.verify(signatureFromMessage);
		} catch (final GeneralSecurityException e) {
			throw new SqrlException(e, "Got exception during EC signature verification");
		}
	}

	/**
	 * Provides the functionality of Apache commons StringUtils.isBlank() without bringing in the dependency
	 *
	 * @param string
	 *            the string to check
	 * @return true if blank, false otherwise
	 */
	public static boolean isBlank(final String string) {
		return string == null || string.trim().length() == 0;
	}

	/**
	 * Provides the functionality of Apache commons StringUtils.isNotBlank() without bringing in the dependency
	 *
	 * @param string
	 *            the string to check
	 * @return true if not blank, false otherwise
	 */
	public static boolean isNotBlank(final String string) {
		return !isBlank(string);
	}

	/**
	 * Internal use only.
	 *
	 * @param ipAddressString
	 *            the ip address to parse
	 * @return the IP address
	 * @throws SqrlException
	 *             if an {@link UnknownHostException} occurs
	 */
	public static InetAddress ipStringToInetAddresss(final String ipAddressString) throws SqrlException {
		if (SqrlUtil.isBlank(ipAddressString)) {
			throw new SqrlException("ipAddressString was null or empty");
		}
		try {
			return InetAddress.getByName(ipAddressString);
		} catch (final UnknownHostException e) {
			throw new SqrlException(e, "Got UnknownHostException for <", ipAddressString, ">");
		}
	}

	/**
	 * Internal use only. Builds a string of name value pairs from the request
	 *
	 * @param servletRequest
	 *            the request
	 * @return a string of the name value pairs that were in the request
	 */
	public static String buildRequestParamList(final HttpServletRequest servletRequest) {
		final Enumeration<String> params = servletRequest.getParameterNames();
		final StringBuilder buf = new StringBuilder(500);
		while (params.hasMoreElements()) {
			final String paramName = params.nextElement();
			buf.append(paramName).append("=").append(servletRequest.getParameter(paramName)).append("  ");
		}
		return buf.toString();
	}

	// TODO: delete - need to decide on cookies first
	public static Cookie createOrUpdateCookie(final HttpServletRequest request, final String cookieDomain,
			final String name, final String value, final int maxAgeInSeconds, final SqrlConfig config) {
		Cookie cookie = findCookie(request, name);
		if (cookie == null) {
			cookie = new Cookie(name, value);
		}
		cookie.setValue(value);
		cookie.setMaxAge(maxAgeInSeconds);
		// secure and httponly are set in applySettingsToCookie
		applySettingsToCookie(cookie, cookieDomain, request, config);
		return cookie;
	}

	private static void applySettingsToCookie(final Cookie cookie, final String cookieDomain,
			final HttpServletRequest request, final SqrlConfig config) {
		if (cookieDomain != null) {
			cookie.setDomain(cookieDomain);
		}
		cookie.setPath(config.getCookiePath());
		cookie.setHttpOnly(false);
		if (request.getScheme().equals(SqrlConstants.SCHEME_HTTPS)) {
			cookie.setSecure(true);
		}
	}

	private static Cookie findCookie(final HttpServletRequest request, final String toFind) {
		if (request.getCookies() != null) {
			for (final Cookie cookie : request.getCookies()) {
				if (toFind.equals(cookie.getName())) {
					return cookie;
				}
			}
		}
		return null;
	}

	public static String findCookieValue(final HttpServletRequest request, final String toFind) {
		final Cookie cookie = findCookie(request, toFind);
		if (cookie == null) {
			return null;
		}
		return cookie.getValue();
	}

	public static void deleteCookies(final HttpServletRequest request, final HttpServletResponse response,
			final SqrlConfig sqrlConfig, final String... cookiesToDelete) {
		final List<String> cookieToDeleteList = Arrays.asList(cookiesToDelete);
		if (request.getCookies() == null) {
			return;
		}
		for (final Cookie cookie : request.getCookies()) {
			if (cookieToDeleteList.contains(cookie.getName())) {
				cookie.setMaxAge(0);
				cookie.setValue("");
				final String cookieDomain = SqrlUtil.computeCookieDomain(request, sqrlConfig);
				applySettingsToCookie(cookie, cookieDomain, request, sqrlConfig);
				response.addCookie(cookie);
			}
		}
	}

	private static final String cookiesToString(final Cookie[] cookieArray) {
		final StringBuilder buf = new StringBuilder(300);
		buf.append("[ ");
		if (cookieArray != null) {
			for (final Cookie cookie : cookieArray) {
				buf.append(cookie.getName()).append("=").append(cookie.getValue()).append(", ");
			}
		}
		return buf.substring(0, buf.length() - 2) + " ]";
	}

	public static String computeCookieDomain(final HttpServletRequest request, final SqrlConfig config) {
		String domain = config.getCookieDomain();
		if (domain == null) {
			final String requestUrl = request.getRequestURL().toString();
			domain = cookieDomainCache.get(requestUrl);
			if (domain == null) {
				// compute the value and store in the cache
				domain = requestUrl.substring(requestUrl.indexOf("//") + 2);
				final int index = domain.indexOf('/');
				if (index > 0) {
					domain = domain.substring(0, index);
				}
				final int portIndex = domain.indexOf(':');
				if (portIndex > -1) {
					domain = domain.substring(0, portIndex);
				}
				cookieDomainCache.put(requestUrl, domain);
			}
			if ("localhost".equals(domain)) {
				return null;
			}
		}
		return domain;
	}

	public static String logEnterServlet(final HttpServletRequest request) {
		if (!logger.isInfoEnabled()) {
			return "";
		}
		final StringBuilder buf = new StringBuilder(300);
		buf.append("In ");
		buf.append(request.getRequestURI()).append(" with params: ");
		buf.append(parameterMapToString(request.getParameterMap())).append(" and cookies: ");
		buf.append(cookiesToString(request.getCookies()));
		return buf.toString();
	}

	public static String parameterMapToString(final Map<String, String[]> parameterMap) {
		final StringBuilder buf = new StringBuilder(400);
		buf.append("<");
		// {c=12850, 38.6=386540,
		for (final Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
			buf.append(entry.getKey()).append(":");
			for (final String aValue : entry.getValue()) {
				buf.append(aValue).append(",");
			}
			buf.append(" ");
		}
		// remove the trailing comma
		if (buf.toString().endsWith(", ")) {
			buf.delete(buf.length() - 2, buf.length());
		}
		buf.append(">");
		return buf.toString();
	}

	/**
	 * Convenience method which concatenates the given objects into a String in an efficient manner using a
	 * {@link StringBuilder} or similar object
	 * 
	 */
	public static String buildString(final Object... objectArray) {
		if (objectArray.length == 0) {
			return "";
		}
		int length = 0;
		for (final Object aObject : objectArray) {
			if (objectArray != null) {
				length += aObject.toString().length();
			} else {
				length += 4;
			}
		}
		final StringBuilder buf = new StringBuilder(length + 1);
		for (final Object aObject : objectArray) {
			if (aObject != null) {
				buf.append(aObject.toString());
			} else {
				buf.append("null");
			}
		}
		return buf.toString();
	}

	public static boolean isClassOnClasspath(final String classToFind) {
		try {
			Class.forName(classToFind);
			return true;
		} catch (final ClassNotFoundException e) {
			return false;
		}
	}

	public static void debugHeaders(final HttpServletRequest servletRequest) {
		if (!logger.isDebugEnabled()) {
			return;
		}
		final StringBuilder buf = new StringBuilder(200);
		final Enumeration<String> iter = servletRequest.getHeaderNames();
		while (iter.hasMoreElements()) {
			final String headerName = iter.nextElement();
			buf.append(headerName).append("=");
			final Enumeration<String> valueIter = servletRequest.getHeaders(headerName);
			boolean another = false;
			while (valueIter.hasMoreElements()) {
				if (another) {
					buf.append(",");
				}
				buf.append(valueIter.nextElement());
				another = true;
			}
			buf.append(" ");
		}
		logger.debug(LoggingUtil.formatForLogging("requestHeaders=[{}]"), buf.toString().trim());
	}

	public static InetAddress findClientIpAddress(final HttpServletRequest servletRequest, final SqrlConfig config)
			throws SqrlException {
		final String ipToParse = findBrowserIpAddressString(servletRequest, config);
		try {
			final InetAddress inetAddress = InetAddress.getByName(ipToParse);
			return inetAddress;
		} catch (final UnknownHostException e) {
			throw new SqrlException(e, "Caught exception trying to determine clients IP address");
		}
	}

	/**
	 * Determines the web browsers IP address by looking at headers (as set in config) or pulling from the
	 * HttpServletRequest
	 */
	public static String findBrowserIpAddressString(final HttpServletRequest servletRequest, final SqrlConfig config) {
		final List<String> headersToCheckList = config.getIpForwardedForHeaderList();
		String ipString = null;
		SqrlUtil.debugHeaders(servletRequest);
		for (final String headerToFind : headersToCheckList) {
			ipString = servletRequest.getHeader(headerToFind);
			if (SqrlUtil.isNotBlank(ipString)) {
				break;
			}
		}
		if (SqrlUtil.isBlank(ipString)) {
			ipString = servletRequest.getRemoteAddr();
		}
		return ipString;
	}

	public static void exceptionIfNull(final String stringData, final String errorDescription) throws SqrlException {
		if (SqrlUtil.isBlank(stringData)) {
			throw new SqrlException(errorDescription);
		}
	}

	public static void exceptionIfNull(final Object data, final String errorDescription) throws SqrlException {
		if (data == null) {
			throw new SqrlException(errorDescription);
		}
	}

	public static boolean isLocalhost(final InetAddress inetAddress) {
		return LOCALHOST_IP_STRING_LIST.contains(inetAddress.toString());
	}
}
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.mockito.Mockito;
import org.springframework.mock.web.MockHttpServletRequest;
import com.github.sqrlserverjava.backchannel.SqrlClientRequest;
import com.github.sqrlserverjava.backchannel.nut.SqrlNutToken0;
import com.github.sqrlserverjava.backchannel.nut.TestCaseSqrlNutHelper;
import com.github.sqrlserverjava.enums.SqrlRequestCommand;
import com.github.sqrlserverjava.enums.SqrlRequestOpt;
import com.github.sqrlserverjava.enums.SqrlServerSideKey;
import com.github.sqrlserverjava.persistence.SqrlAutoCloseablePersistence;
import com.github.sqrlserverjava.persistence.SqrlCorrelator;
import com.github.sqrlserverjava.persistence.SqrlJpaPersistenceProvider;
import com.github.sqrlserverjava.util.SqrlConstants;
import com.github.sqrlserverjava.util.SqrlServiceExecutor;
import com.github.sqrlserverjava.util.SqrlUtil;
import junitx.util.PrivateAccessor;
package com.github.sqrlserverjava;






public class TestCaseUtil {
	public static final Date AWHILE_FROM_NOW = new Date(System.currentTimeMillis() + 1000000);
	static final String DEFAULT_CONFIG_SQRL_BACKCHANNEL_PATH = "http://127.0.0.1:8080/sqrlbc";
	static final String AES_TEST_KEY = Base64.getEncoder().encodeToString(new byte[16]);

	static class TCSqrlConfig extends SqrlConfig {
		private final long timestampForNextNut;

		private TCSqrlConfig(final long timestampForNextNut) {
			this.timestampForNextNut = timestampForNextNut;
		}

		@Override
		public long getCurrentTimeMs() {
			return timestampForNextNut;
		}
	}

	public static final SqrlConfig buildTestSqrlConfig(final String nutString) throws Exception {
		final SqrlConfig config = new TCSqrlConfig(System.currentTimeMillis());
		// Set SqrlServerOperations counter to generate the expected value
		// final ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES);
		// buffer.putInt(nutToken.getRandomInt());
		// config.setSecureRandom(new TestSecureRandom(buffer.array())); // TODO: fix or remove?
		config.setSecureRandom(new TestSecureRandom(null));

		config.setBackchannelServletPath("http://127.0.0.1:8080/sqrlbc");
		// set AES key to all zeros for test cases
		config.setAesKeyBase64(AES_TEST_KEY);
		config.setCpsCancelUri("www.google.com");

		return config;
	}

	public static final SqrlConfig buildTestSqrlConfig() {
		final SqrlServiceExecutor executor = new SqrlServiceExecutor();