AuthFsm.java
/***************************************************************************
Copyright 2013 Emily Estes
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.
***************************************************************************/
package net.metanotion.formsauth;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.metanotion.authident.AuthUtils;
import net.metanotion.authident.UserToken;
import net.metanotion.util.EventCalculator;
import net.metanotion.util.EventHandler;
import net.metanotion.util.State;
import net.metanotion.util.StateMachine;
import net.metanotion.util.StateMachineFactory;
import net.metanotion.util.Unknown;
import net.metanotion.web.SessionFactory;
import net.metanotion.web.concrete.StateMachineSessionFactory;
/** This class implements a two-state Finite State Machine for the forms auth to handle most basic use cases.
The forms/basic auth library uses a state machine abstraction to coordinate control of session state. However
many applications just have two states: An authenticated user session(with a current user id) and an unauthenticated
session. This class handles the logic for that and wraps your app sessions with a simple FSM. Your app
must implement the AuthStates interface, which has two methods for getting the basic states(authenticated user
and unathenticated), and another method for generating new unauthenticated sessions.
@param <App> The application specific type of the data type provided to the app's
{@link net.metanotion.formsauth.AuthStates#newSession} method. Typically will be something like the main
application class type.
@param <Session> The application specific session instance/component type.
*/
public final class AuthFsm<App,Session extends Unknown>
implements StateMachineFactory<AuthFsm.SessionState, Map.Entry<StateMachine,App>>, SessionFactory<App> {
private static final Logger logger = LoggerFactory.getLogger(AuthFsm.class);
/** A Two-state finite state machine for authentication. This machine will wrap the session object returned from
the {@link AuthStates#newSession} method. This class is protected instead of private so we can use it in the
type parameters to the outer class as the concrete {@link net.metanotion.util.State} type for the
{@link net.metanotion.util.StateMachineFactory} type parameter. */
protected static final class SessionState implements State<SessionState> {
private final EventCalculator<SessionState> ec;
private final Unknown session;
/** Create a new finite state machine.
@param ec The event-calculator for computing next states for this machine.
@param session The underlying session to wrap with this machine.
*/
public SessionState(final EventCalculator<SessionState> ec, final Unknown session) {
this.ec = ec;
this.session = session;
}
@Override public SessionState nextState(final Object event) { return ec.nextState(event, this); }
@Override public <I> I lookupInterface(final Class<I> theInterface) {
if(theInterface == Object.class) { return (I) this; }
if(theInterface == Unknown.class) { return (I) this; }
return session.lookupInterface(theInterface);
}
}
/** An event handler to use in an event calculator that uses {@link AuthStates#login} for the next state. */
private static final class LogIn implements EventHandler<SessionState> {
private final AuthStates as;
/** Create the event handler.
@param as The AuthStates instance to back this handler with. */
public LogIn(final AuthStates as) { this.as = as; }
@Override public SessionState nextState(final Object event, final SessionState currentState) {
logger.debug("Log in state event");
return new SessionState(currentState.ec, as.login((UserToken) event, currentState.session));
}
};
/** An event handler to use in an event calculator that uses {@link AuthStates#logout} for the next state. */
private static final class LogOut implements EventHandler<SessionState> {
private final AuthStates as;
/** Create the event handler.
@param as The AuthStates instance to back this handler with. */
public LogOut(final AuthStates as) { this.as = as; }
@Override public SessionState nextState(final Object event, final SessionState currentState) {
logger.debug("Log out state event");
return new SessionState(currentState.ec, as.logout(currentState.session));
}
};
private final EventCalculator<SessionState> ec = new EventCalculator<SessionState>();
private final StateMachineSessionFactory<App, SessionState> factory = new StateMachineSessionFactory<App, SessionState>(this);
private final AuthStates<App, Session> as;
private static final class W<App> implements SessionFactory<App>, Unknown {
private final SessionFactory<App> factory;
private final Unknown server;
public W(final SessionFactory<App> factory, final Unknown server) {
this.factory = factory;
this.server = server;
}
@Override public Unknown newSession(final App app) { return this.factory.newSession(app); }
@Override public <I> I lookupInterface(final Class<I> theInterface) { return this.server.lookupInterface(theInterface); }
}
/** Wrap an AuthStates instance and Unknown service provider in to a session factory instance.
@param <App> The type of the web application associated with the AuthStates implementation.
@param <Session> The session type of the web application.
@param <A> The type of the session factory returned by this method.
@param as The user provided implementation of AuthStates to generate state/session instances.
@param u The service provider instance backing the session factory.
@return The session factory instance that also implements Uknown.
*/
public static <App, Session extends Unknown, A extends SessionFactory<App> & Unknown> A wrap(
final AuthStates<App, Session> as,
final Unknown u) {
return (A) new W<App>(new AuthFsm<>(as), u);
}
/** Create a finite state machine server using an instance of AuthStates to fill in the details.
@param as The user provided implementation of AuthStates to generate state/session instances.
*/
public AuthFsm(final AuthStates<? extends App, Session> as) {
this.as = (AuthStates<App,Session>) as;
AuthUtils.addEvents(ec, new LogIn(as), new LogOut(as));
}
// SessionFactory
@Override public Unknown newSession(final App a) { return factory.newSession(a); }
// StateMachineFactory, called by factory.newSession(...)
@Override public SessionState newMachine(final Map.Entry<StateMachine,App> startup) {
return new SessionState(ec, as.newSession(startup.getValue(), startup.getKey()));
}
}