at.spardat.xma.component.Component Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
package at.spardat.xma.component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import at.spardat.enterprise.exc.SysException;
import at.spardat.xma.mdl.Atom;
import at.spardat.xma.mdl.IFormattable;
import at.spardat.xma.mdl.Synchronization;
import at.spardat.xma.mdl.util.DNode;
import at.spardat.xma.mdl.util.Descriptive;
import at.spardat.xma.mdl.util.TransAtomTable;
import at.spardat.xma.page.Page;
import at.spardat.xma.serializer.XmaInput;
import at.spardat.xma.serializer.XmaOutput;
import at.spardat.xma.session.XMASession;
/**
* This class implements the comon base for Components on the client
* side and on the server side. This is mostly the live-cycle-management
* of the PageModels.
*
* @author s2877
*/
public abstract class Component implements Synchronization, Descriptive {
/**
* contains all active PageModels of the Component
*/
HashMap pageModels = new HashMap();
/**
* Component properties are stored in instance-variables in generated
* subclasses. For the purpose of synchronizing properties between
* client and server, this transactional one-column table is used.
* In RPCs, properties are copied from the propModel to instance
* variables and vice versa usind the methods prop2model
* and model2props.
*/
private TransAtomTable propModel = new TransAtomTable(1);
/**
* counter for assigning unique IDs to PageModels
*/
short lastPageId = 0;
/**
* Indicates whether this Component is stateless or not. A Component may
* only be stateless if all its pages are stateless. Stateless Components
* do not consume space at the server before and after a server side event.
* This also means that the programmer cannot store information in the class
* derived from ComponentServer that survives server side events.
* This property is defined at UI design time and may not be changed at
* programming time or later.
*/
private boolean isStateless_;
/**
* A dynamically (at runtime) assigned id which is unique amongst all living
* component instances within a Session. The id is created at the client side
* of XMA, since Components are also created there.
*/
private short id_;
/**
* The session this component belongs to. Never null.
*/
protected XMASession session_;
/**
* This instance variable applies to stateful components only.
* Synchronizing component state between client and server is based
* on exchanging delta information for bandwith efficiency. This
* requires that client and server side state are correct with respect
* to each other in order to successfully apply deltas.
*
* There are certain types of communication failures where the client
* cannot make any assumptions about the state of the server component,
* i.e., the client does not know whether the changes in the last
* server side event have been successfully applied or not.
*
* The purpose of the serverChangeNumber_ (SCN) is to detect this
* kind of failures. The server component increments the counter
* once if it has successfully integrated changes he's got from the client
* in the course of a server side event. The server component increments
* the number a second time if changes made by the programmer on the
* models are committed.
*
* The number is transmitted back to the client and retransmitted in
* the next server side event to make sure that the server components
* state is exactly the same the client component thinks it is.
*
* If there are communication failures, the client may guess what the
* server did: Either
*
* - the server did nothing, then SCN would not have changed.
*
- the server integrated the changes, then SCN would have increased by one.
*
- the server successfully did the business logic, then SCN increased by two.
*
* Whatever the client guesses, if he is wrong, the component states are
* inconsistent with respect to each other. In this case, the user
* should be forced to cancel the component since there is no easy
* way out. Period.
*
* A value of -1 means that the number is unknown. This is the case
* with stateless components, where the mechanism is not used. Or if
* the component is new at the client, where there has been none defined
* yet.
*/
private short serverChangeNumber_ = -1;
/**
* Constructor
*
* @param session the XMASession this component belongs to
* @param isStateless defines if this component is stateless
* @param isAtServer indicates if this component executes at the server side
*/
public Component (XMASession session, boolean isStateless, boolean isAtServer) {
isStateless_ = isStateless;
if (session == null) {
throw new IllegalArgumentException("session must not be null!");
}
session_ = session;
}
/**
* This is a noop-constructor for test-purpose.
*/
public Component () {
}
/**
* Returns true if this Component is stateless. Stateless Components
* do not consume space at the server before and after a server side event.
*/
public boolean isStateless () {
return isStateless_;
}
/**
* Registers the given Page in the Component and assignes a unique ID to it.
* A page must be registered at the time its models are created.
* After registration, the Page will be syncronized between client and server
* every time a serverEvent occures until the page is removed.
*
* The assigned id is also stored in the page (via Page.setId()).
*
* @param model the Page to register.
* @throws IllegalStateExecption if the page is already registered.
*/
public void registerPageModel (Page model) {
if(pageModels.get(new Integer(model.getId()))!=null) {
throw new IllegalStateException("PageModel allready allocated");
}
model.setId (++lastPageId);
pageModels.put(new Integer(lastPageId), model);
}
/**
* Gets the Page with the given ID.
* The Page exists, if it has been registered and not removed yet.
*
* @param idPage the ID of the desired Page.
* @return the Page with the given ID or null if there exists no Page
* for the given ID.
*/
public Page getPageModel (short idPage) {
return (Page) pageModels.get(new Integer(idPage));
}
/**
* Indicates if there exists an active Page with the given ID.
* The Page is alive, if it has been registered and not removed yet.
*
* @param idPageModel the ID of the Page
* @return true if the Page with the given ID is alive
* false otherwise.
*/
public boolean isPageModelAlive(short idPageModel) {
return pageModels.containsKey(new Integer(idPageModel));
}
/**
* Removes a page model with a given id. The model aspect of this
* page effectively is dead afterwards. The data stored in widget
* models on this page is lost.
*
* @param idPageModel the id of the model to remove
* @return true if successfully removed, false if there does not
* exist a page model with the provided id.
*/
protected boolean removePageModel (short idPageModel) {
return pageModels.remove (new Integer (idPageModel)) != null;
}
/**
* Gets the number of registered PageModels in this Component.
*
* @return the number of PageModels registered but not removed yet.
*/
public int getNumPageModels() {
return pageModels.size();
}
/**
* Gets an Iterator to iterate over all living PageModels.
*
* @return an Iterator to iterate over all living PageModels.
*/
public Iterator getPageModels() {
return pageModels.values().iterator();
}
/**
* Gets the full classname for the given typeId of
* a page in the component. This method will be implemented
* by the generated class for each component.
*
* @param typeId the unique typeId
* @return the full classname
*/
abstract public String getModelClass(short typeId);
/**
* Writes a set of pages to the output stream.
*
* @param pages the set of pages
* @param xo the output stream
* @param forceFull if true, all pages with all widget models are written. If false,
* only changed pages and within these pages only changed widget
* models are written.
*/
protected void externalizePageSet (HashSet pages, XmaOutput xo, boolean forceFull) throws IOException {
Iterator iter = pages.iterator();
while (iter.hasNext()) {
Page p = (Page) iter.next();
// write the page if forceFull mandates it or the page changed.
boolean writePage = forceFull || p.changed();
// a stateless page is always transferred from client to server
if (!xo.isAtServer() && p.isStateless()) writePage = true;
if (writePage) {
// write true in front of the page which indicates that a page follows
xo.writeBoolean("pageFollows", true);
// write id
xo.writeShort("id", p.getId());
// write the page;
boolean forceFullOnPage = forceFull;
// from client to server stateless pages are always fully written, even
// if forceFull is false.
if (!xo.isAtServer() && p.isStateless()) forceFullOnPage = true;
p.externalize (xo, forceFullOnPage);
}
}
// write false at the end to signal the reader to stop right after here
xo.writeBoolean("pageFollows", false);
}
/**
* Reads the information produces via externalizePageSet and applies
* the changes to the models.
*
* This method requires that all pages whose information will be read out
* of the stream are already created.
*
* @param xi the object input stream
* @throws IOException on serialization errors
* @throws ClassNotFoundException on serialization errors
*/
protected void internalizePageSet (XmaInput xi, boolean collectFormatter) throws IOException, ClassNotFoundException {
// read pages as long as we read a true-boolean
boolean morePages = xi.readBoolean();
if (morePages) {
Map pageFormatters = new LinkedHashMap();
while (morePages) {
short pageId = xi.readShort();
// retrieve the page
Page p = getPageModel(pageId);
// p == null clearly is a programming error
if (p == null) throw new RuntimeException ("Page " + pageId + " does not exist");
List listFormattable = null;
if (collectFormatter) {
listFormattable = new ArrayList();
}
p.internalize(xi, listFormattable);
if (collectFormatter) {
pageFormatters.put(p, listFormattable);
}
morePages = xi.readBoolean();
}
for (Iterator i = pageFormatters.entrySet().iterator(); i.hasNext(); ) {
Entry entry = (Entry) i.next();
List listFormattable = (List) entry.getValue();
for (Iterator j = listFormattable.iterator(); j.hasNext(); ) {
IFormattable iFormattable = (IFormattable) j.next();
if (!iFormattable.isValid()) {
throw new SysException("Server side validation failed!");
}
}
}
}
}
/**
* Writes the property model to xout.
*
* @param xo output-stream
* @param forceFull ignore deltas
*/
protected void externalizeProperties (XmaOutput xo, boolean forceFull) throws IOException {
// write the pageModel if forceFull mandates it or the propModel changed.
boolean write = forceFull || propModel.changed();
xo.writeBoolean ("propWrite", write);
if (write) {
propModel.externalize (xo, forceFull);
}
}
/**
* modifies the propModel so that the value of the cell (i,0) equals
* the ith property of this component.
*
* This method must be called from outside the XMA-framework.
*/
public void props2model () {
ComponentProperty [] cps = getPropDes();
int numProps = (cps == null ? 0 : cps.length);
for (int i=0; i
*
* This method must not be called from outside the XMA-framework.
*/
public void model2props () {
ComponentProperty [] cps = getPropDes();
int numProps = (cps == null ? 0 : cps.length);
for (int i=0; iindex to value.
* Expands the model to contain at least index+1 rows if necessary.
* Does not modify the model if the old value equals the provided one.
*
* @param index index of the property
* @param value new value which may be null
*/
private void setPropModelValue (int index, String value) {
// expand model if necessary
while (propModel.size() < index+1)
propModel.add (String.valueOf(propModel.size()), new Atom[]{null});
// get old value
String oldValue = getPropModelValue (index);
if (!eqString (value, oldValue)) {
if (value == null) propModel.replace(index, 0, null);
else propModel.replace(index, 0, new Atom (value));
}
}
/**
* Returns the value of the property at index.
*/
private String getPropModelValue (int index) {
if (index >= propModel.size()) return null;
Atom valueAsAtom = propModel.get(index, 0);
return (valueAsAtom == null ? null : valueAsAtom.toString());
}
// checks if two strings are equal, including the treatment of null Strings.
// two null Strings are considered equal. A null String and a non null String
// are not equal.
private static boolean eqString (String s1, String s2) {
if (s1 == null) return s2 == null;
else return s1.equals(s2);
}
/**
* Restores property model externalized via externalizeProperties.
*
* @param xi input stream
*/
protected void internalizeProperties (XmaInput xi) throws IOException, ClassNotFoundException {
boolean read = xi.readBoolean();
if (read) {
propModel.internalize(xi, null);
}
}
/**
* @see at.spardat.xma.mdl.util.Descriptive#describe(at.spardat.xma.mdl.util.DNode)
*/
public void describe (DNode n) {
n.app("[").app(id_).app("] ");
n.appShortClass(this).app(": ");
n.app("stateless", isStateless_);
Iterator iter = getPageModels();
while (iter.hasNext()) {
Page pm = (Page) iter.next();
DNode son = new DNode(n);
pm.describe(son);
}
}
/**
* @see java.lang.Object#toString()
*/
public String toString () {
return DNode.toString(this);
}
/**
* Returns the id of this component.
*/
public short getId () {
return id_;
}
/**
* Sets the id of this component. This method must not be called from outside
* the XMA-framework.
*/
public void setId (short s) {
id_ = s;
}
/**
* Returns the short name of this component. The short name uniquely identifies
* a component type within an XMA application. Furthermore, the shortname equals
* the last component in the components fully qualified class name.
*/
public String getName () {
String className = getClass().getName();
int lastPoint = className.lastIndexOf('.');
return lastPoint != -1 ? className.substring(lastPoint+1) : className;
}
/**
* Returns the server change number.
*/
public short getSCN () {
return serverChangeNumber_;
}
/**
* Sets the server change number.
*/
public void setSCN (short s) {
serverChangeNumber_ = s;
}
/**
* Increments the server change number.
*/
public void incrementSCN () {
serverChangeNumber_++;
}
/**
* Calls commit on every page in this component. This method must not be called
* from outside the framework, nor overwritten.
*/
public void commit () {
Iterator iter = getPageModels();
while (iter.hasNext()) {
Page p = (Page) iter.next();
p.commit();
}
// and the property model
propModel.commit();
}
/**
* Calls rollback on every page in this component. This method must not be called
* from outside the framework, nor overwritten.
*/
public void rollback () {
Iterator iter = getPageModels();
while (iter.hasNext()) {
Page p = (Page) iter.next();
p.rollback();
}
// and the property model
propModel.rollback();
}
/**
* @return the descriptors of all properties of this component.
* null if no properties are defined for the component in the guidesigner .
*/
protected ComponentProperty[] getPropDes() {
return null;
}
/**
* Gets the descriptor of a given property
* Only properties defined in the guidesigner for this component
* are found by this method.
* @param name the name of the property to find
* @return the descriptor of the property with the given name
*/
protected ComponentProperty getPropDes(String name) {
ComponentProperty[] allprops = getPropDes();
if(allprops!=null) {
for(int i=0;i