/*--------------------------------------------------------------------------*
| Copyright (C) 2006 Christopher Kohlhaas |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by the |
| Free Software Foundation. A copy of the license has been included with |
| these distribution in the COPYING file, if not go to www.fsf.org |
| |
| As a special exception, you are granted the permissions to link this |
| program with every library, which license fulfills the Open Source |
| Definition as published by the Open Source Initiative (OSI). |
*--------------------------------------------------------------------------*/
package org.rapla.storage.dbrm;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import org.apache.avalon.framework.configuration.Configuration;
import org.rapla.components.util.Command;
import org.rapla.components.util.CommandQueue;
import org.rapla.components.util.SerializableDateTimeFormat;
import org.rapla.entities.EntityNotFoundException;
import org.rapla.entities.RaplaType;
import org.rapla.entities.User;
import org.rapla.entities.storage.EntityResolver;
import org.rapla.entities.storage.RefEntity;
import org.rapla.entities.storage.internal.SimpleEntity;
import org.rapla.entities.storage.internal.SimpleIdentifier;
import org.rapla.framework.Container;
import org.rapla.framework.RaplaContext;
import org.rapla.framework.RaplaException;
import org.rapla.plugin.RaplaExtensionPoints;
import org.rapla.server.RemoteMethod;
import org.rapla.server.RemoteServiceCaller;
import org.rapla.server.RemoteStorage;
import org.rapla.server.RestartServer;
import org.rapla.server.internal.RemoteStorageImpl;
import org.rapla.server.internal.SessionExpiredException;
import org.rapla.storage.IOContext;
import org.rapla.storage.LocalCache;
import org.rapla.storage.RaplaSecurityException;
import org.rapla.storage.UpdateEvent;
import org.rapla.storage.UpdateResult;
import org.rapla.storage.impl.AbstractCachableOperator;
import org.rapla.storage.impl.EntityStore;
import org.rapla.storage.xml.RaplaInput;
import org.rapla.storage.xml.RaplaMainReader;
/** This operator can be used to modify and access data over the
* network. It needs an server-process providing the StorageService
* (usually this is the default rapla-server).
* <p>Sample configuration:
<pre>
<remote-storage id="web">
</remote-storate>
</pre>
* The messaging-client value contains the id of a
* messaging-client-component which handles the
* communication with the server.
* The RemoteOperator provides also the Service {@link RemoteServiceCaller}
* @see org.rapla.server.RemoteStorageCallback
* @see org.rapla.components.rpc.MessagingClient
*/
public class RemoteOperator
extends
AbstractCachableOperator
implements
RemoteServiceCaller
,RestartServer
{
ServerStub serv = new ServerStub();
String username;
String password;
protected CommandQueue notifyQueue;
private boolean bSessionActive = false;
Connector connector;
private boolean bReservationsFetched;
private Date firstCachedDate = null;
private Date lastCachedDate = null;
private boolean isRestarting;
public RemoteOperator(RaplaContext context, Configuration config) throws RaplaException {
super( context );
Container raplaMainContainer = ((Container)context.lookup( Container.ROLE));
raplaMainContainer.addContainerProvidedComponent( RaplaExtensionPoints.SERVLET_PAGE_EXTENSION, RaplaStorePage.class.getName(), "store", null);
((Container)context.lookup( Container.ROLE)).addContainerProvidedComponentInstance(RestartServer.ROLE, this);
((Container)context.lookup( Container.ROLE)).addContainerProvidedComponentInstance(RemoteServiceCaller.ROLE, this);
connector = new HTTPConnector(context,config);
}
public void connect() throws RaplaException {
throw new RaplaException("RemoteOperator doesn't support anonymous connect");
}
public void connect(String username,char[] password) throws RaplaException {
this.username = username;
this.password = new String(password);
if (isConnected())
return;
getLogger().info("Connecting to server and starting login..");
doConnect();
try {
String clientVersion= i18n.getString("rapla.version") ;
serv.checkServerVersion( clientVersion);
serv.login(this.username,this.password);
bSessionActive = true;
updateToday();
getLogger().info("login successfull");
} catch (RaplaException ex){
disconnect();
throw ex;
}
loadData();
notifyQueue = org.rapla.components.util.CommandQueue.createCommandQueue();
}
public void saveData() throws RaplaException {
throw new RaplaException("RemoteOperator doesn't support storing complete cache, yet!");
}
/** implementation specific. Should be private */
public void serverHangup() {
getLogger().warn("Server hangup");
if (!isRestarting) {
getLogger().error(getI18n().format("error.connection_closed",getConnectionName()));
}
isRestarting = false;
new Thread() {
public void run() {
fireStorageDisconnected();
}
}.start();
}
public String getConnectionName() {
return connector.getInfo();
}
private void doConnect() throws RaplaException {
boolean bFailed = true;
try {
connector.start();
bFailed = false;
} catch (Exception e) {
throw new RaplaException(i18n.format("error.connect",getConnectionName()),e);
} finally {
if (bFailed)
disconnect();
}
}
public boolean isConnected() {
// return connector.hasSession();//messagingClient != null && messagingClient.isRunning();
return bSessionActive;
}
public boolean supportsActiveMonitoring() {
return true;
}
public void refresh() throws RaplaException {
serv.refresh();
}
public void restartServer() throws RaplaException {
isRestarting = true;
serv.restartServer();
fireStorageDisconnected();
}
/** disconnect from the server */
public void disconnect() throws RaplaException {
getLogger().info("Disconnecting from server");
try {
bSessionActive = false;
if ( notifyQueue != null)
{
notifyQueue.dequeueAll(); // Execute all update Commands.
}
firstCachedDate = null;
lastCachedDate = null;
bReservationsFetched = false;
connector.stop();
cache.clearAll();
} catch (Exception e) {
throw new RaplaException("Could not disconnect", e);
}
fireStorageDisconnected();
}
private void addToCache(List list, boolean useCache) throws RaplaException {
EntityResolver entityResolver = createEntityResolver( list, useCache ? cache : null );
synchronized (cache) {
resolveEntities( list.iterator(), entityResolver );
for( Iterator it = list.iterator();it.hasNext();) {
SimpleEntity entity = (SimpleEntity) it.next();
cache.put(entity);
}
}
}
private void loadData() throws RaplaException {
checkConnected();
cache.clearAll();
getLogger().debug("Getting Data..");
// recontextualize Entities
addToCache(serv.getResources(), false );
getLogger().debug("Data flushed");
}
protected void checkConnected() throws RaplaException {
if ( !bSessionActive ) {
if (username == null) {
throw new RaplaException("Need to login first!");
} else {
throw new RaplaException(i18n.format("error.connection_closed", getConnectionName()));
}
}
}
protected long getCurrentTime() throws RaplaException {
if ( bSessionActive )
return serv.getServerTime();
else
return super.getCurrentTime();
}
public void dispatch(UpdateEvent evt) throws RaplaException {
checkConnected();
// Create closure
UpdateEvent closure = createClosure(evt );
check( closure );
// Store on server
if (getLogger().isDebugEnabled()) {
Iterator it =closure.getStoreObjects().iterator();
while (it.hasNext()) {
RefEntity entity = (RefEntity)it.next();
getLogger().debug("dispatching store for: " + entity);
}
it =closure.getRemoveObjects().iterator();
while (it.hasNext()) {
RefEntity entity = (RefEntity)it.next();
getLogger().debug("dispatching remove for: " + entity);
}
}
serv.dispatch( closure );
// Store in cache
UpdateResult result = update( closure, true );
fireStorageUpdated(result);
}
public Object createIdentifier(RaplaType raplaType) throws RaplaException {
return serv.createIdentifier(raplaType);
}
/** we must override this method because we can't store the passwords on the client*/
public void authenticate(String username,String password) throws RaplaException {
serv.authenticate(username,password);
}
public boolean canChangePassword() {
try {
return serv.canChangePassword();
} catch (RaplaException ex) {
return false;
}
}
public void changePassword(User user,char[] oldPassword,char[] newPassword) throws RaplaException {
try {
serv.changePassword(user.getUsername(),oldPassword,newPassword);
} catch (RaplaSecurityException ex) {
throw new RaplaSecurityException(i18n.getString("error.wrong_password"));
}
}
private void updateReservations(User user,Date start,Date end) throws RaplaException {
if ( !bReservationsFetched ) {
bReservationsFetched = true;
firstCachedDate = start;
lastCachedDate = end;
addToCache(serv.getReservations(firstCachedDate, lastCachedDate), true );
return;
}
if ( firstCachedDate != null) {
if (start == null || start.before(firstCachedDate)) {
addToCache(serv.getReservations( start, firstCachedDate), true );
firstCachedDate = start;
}
}
if ( lastCachedDate != null) {
if (end == null || end.after(lastCachedDate)) {
addToCache(serv.getReservations( lastCachedDate, end), true );
lastCachedDate = end;
}
}
}
public RefEntity resolveId(Object id) throws EntityNotFoundException {
try {
return super.resolveId(id);
} catch (EntityNotFoundException ex) {
try {
List resolved = serv.getEntityRecursive( id );
addToCache(resolved, true );
} catch (RaplaException rex) {
throw new EntityNotFoundException("Object for id " + id.toString() + " not found due to " + ex.getMessage());
}
return super.resolveId(id);
}
}
public SortedSet getAppointments(User user,Date start,Date end) throws RaplaException {
checkConnected();
updateReservations( user, start, end );
return super.getAppointments( user, start, end );
}
public List getReservations(User user,Date start,Date end) throws RaplaException {
checkConnected();
updateReservations( user, start, end );
return super.getReservations( user, start, end);
}
private String readResultToString( InputStream input) throws IOException
{
InputStreamReader in = new InputStreamReader( input);
char[] buf = new char[4096];
StringBuffer buffer = new StringBuffer();
while ( true )
{
int len = in.read(buf);
if ( len == -1)
{
break;
}
buffer.append( buf, 0,len );
//buf.
}
String result = buffer.toString();
return result;
}
public String call( String serviceName, RemoteMethod method, String[] args ) throws RaplaException
{
return serv.call(serviceName,method, (String[])args);
}
long clientRepositoryVerion = 0;
public class ServerStub implements RemoteStorage {
String call( RemoteMethod method,String[] args) throws RaplaException {
return call( null, method, args);
}
InputStream callInput( RemoteMethod method,String[] args) throws RaplaException {
return callInput( null, method, args);
}
String call( String service, RemoteMethod method,String[] args) throws RaplaException {
try {
InputStream stream = callInput( service,method,args);
String result = readResultToString( stream);
// System.out.println( result );
return result;
} catch (IOException ex) {
throw new RaplaException(ex);
}
}
InputStream callInput( String service,RemoteMethod method,String[] args) throws RaplaException {
try {
String methodName = method.method();
Map argMap = createArgumentMap( method, args);
if ( service != null)
{
methodName = service +"/" + methodName;
}
return connector.call( methodName, argMap );
} catch (SessionExpiredException ex) {
disconnect();
throw ex;
} catch (IOException ex) {
throw new RaplaException(ex);
}
}
private Map createArgumentMap( RemoteMethod method, String[] args ) throws RaplaException
{
Map argMap = new HashMap();
if ( args.length != method.length())
{
throw new RaplaException("Paramter list don't match Expected " + method.length() +" but was " + args.length);
}
for ( int i=0;i<args.length;i++)
{
String argName = method.arg( i );
argMap.put(argName, args[i]);
}
return argMap;
}
public void login(String username,String password) throws RaplaException {
call(LOGIN,new String[] { username,password});
}
public void checkServerVersion(String clientVersion) throws RaplaException {
call(CHECK_SERVER_VERSION, new String[] {clientVersion});
}
public void authenticate(String username,String password) throws RaplaException {
String[] args = new String[] { username,password};
call(AUTHENTICATE,args);
}
public long getServerTime() throws RaplaException {
String timeString = call(GET_SERVER_TIME,new String[] {});
return Long.parseLong( timeString);
}
public boolean canChangePassword() throws RaplaException {
String result = call(CAN_CHANGE_PASSWORD, new String[] {});
return Boolean.valueOf( result).booleanValue();
}
public void changePassword(String username,char[] oldPassword,char[] newPassword) throws RaplaException {
String oldPasswordS = new String(oldPassword);
String newPasswordS = new String(newPassword);
call(CHANGE_PASSWORD,new String[] {username, oldPasswordS, newPasswordS});
}
public List getEntityRecursive(Object id) throws RaplaException {
SimpleIdentifier castedId = (SimpleIdentifier) id;
String idS = castedId.getTypeName() + "_" + String.valueOf(castedId.getKey());
InputStream stream = callInput( GET_ENTITY_RECURSIVE, new String[] {idS});
EntityStore store = new EntityStore( cache, cache.getSuperCategory());
return readIntoStore( stream, store );
}
public List getResources() throws RaplaException {
InputStream stream = callInput( GET_RESOURCES,new String[] {});
EntityStore store = new EntityStore( null, cache.getSuperCategory());
return readIntoStore( stream, store );
}
public List getReservations(Date start,Date end) throws RaplaException {
SerializableDateTimeFormat format = new SerializableDateTimeFormat();
String startS = null;
String endS = null;
if (start!= null)
{
startS = format.formatDate( start);
}
if ( end != null )
{
endS = format.formatDate( end);
}
InputStream stream = callInput( GET_RESERVATIONS,new String[] {startS, endS});
EntityStore store = new EntityStore( cache, cache.getSuperCategory());
return readIntoStore( stream, store );
}
private List readIntoStore( InputStream stream, EntityStore store ) throws RaplaException
{
RaplaContext inputContext = new IOContext().createInputContext(serviceManager,store,idTable);
RaplaInput xmlAdapter = new RaplaInput( getLogger().getChildLogger("reading"));
RaplaMainReader contentHandler = new RaplaMainReader( inputContext);
try
{
xmlAdapter.read(new InputStreamReader( stream,"UTF-8"), contentHandler, false);
}
catch (IOException e)
{
throw new RaplaException( "Error retrieving Data ", e);
}
return new ArrayList(store.getList());
}
public Object createIdentifier(RaplaType raplaType) throws RaplaException {
String raplaTypeS = raplaType.toString();
String id = call(CREATE_IDENTIFIER, new String[] {raplaTypeS});
try
{
return LocalCache.getId( raplaType, id);
}
catch (ParseException e)
{
throw new RaplaException( "Invalid id Object", e);
}
}
public void dispatch(UpdateEvent evt) throws RaplaException {
try
{
String xml = RemoteStorageImpl.createUpdateEvent( serviceManager,cache,evt );
call( DISPATCH, new String[] {xml});
}
catch (IOException e)
{
throw new RaplaException( "Error retrieving Data ", e);
}
}
public void restartServer() throws RaplaException {
call(RESTART_SERVER,new String[] {});
}
public void refresh() throws RaplaException {
String xml = call( REFRESH,new String[] {String.valueOf(clientRepositoryVerion)});
if ( xml.length() < 50 && xml.indexOf( "<uptodate/>")>=0)
{
}
else
{
RemoteOperator.this.refresh( xml);
}
}
public void notifyUpdate() {
if ( isRestarting)
return;
notifyQueue.enqueue(new UpdateCommand());
}
}
private void refresh(String xml) throws RaplaException
{
synchronized (getLock())
{
UpdateEvent evt = RemoteStorageImpl.createUpdateEvent( serviceManager,xml, cache );
Iterator it = evt.getStoreObjects().iterator();
while (it.hasNext()) {
SimpleEntity entity = (SimpleEntity) it.next();
RefEntity cachedVersion = (RefEntity) cache.get(entity.getId());
// Ignore object if its not newer than the one in cache.
if (cachedVersion != null && cachedVersion.getVersion() >= entity.getVersion()) {
//getLogger().debug("already on client " + entity + " version " + cachedVersion.getVersion());
it.remove();
continue;
}
if (getLogger().isDebugEnabled())
getLogger().debug(" storing " + entity.getId()
+ " version: " + entity.getVersion());
}
RemoteOperator.super.resolveEntities
(
evt.getStoreObjects().iterator()
,createEntityResolver(evt.getStoreObjects(),cache)
);
it = evt.getRemoveObjects().iterator();
while (it.hasNext()) {
SimpleEntity entity = (SimpleEntity) it.next();
RefEntity cachedVersion = (RefEntity) cache.get(entity.getId());
// Ignore object, if its not in cache.
if (cachedVersion == null) {
it.remove();
continue;
}
if (getLogger().isDebugEnabled())
getLogger().debug(" removing " + entity.getId()
+ " version: " + entity.getVersion());
}
RemoteOperator.super.resolveEntities
(
evt.getRemoveObjects().iterator()
,createEntityResolver(evt.getStoreObjects(),cache)
);
if ( bSessionActive &&
( evt.getRemoveObjects().size() > 0
|| evt.getStoreObjects().size() > 0 ) ) {
getLogger().info("Objects updated!");
UpdateResult result = update(evt, false);
clientRepositoryVerion = evt.getRepositoryVersion();
// now we can set the cache as updated
fireStorageUpdated(result);
}
clientRepositoryVerion = evt.getRepositoryVersion();
}
}
public void serverDisconnected() {
bSessionActive = false;
}
//******* End ClientInterface *************
class UpdateCommand implements Command {
public void execute() {
if ( !bSessionActive )
return; // We can ignore the update!
try {
serv.refresh();
} catch (Exception ex) {
getLogger().error(ex.getMessage(),ex);
/*
// #TODO. Do we need do disconnect on every notify error?
try {
disconnect();
} catch (RaplaException rex) {
getLogger().error(rex.getMessage(),rex);
}
*/
}
}
}
}
|