Repository for FPT-Excercises
Aktuell ist vorgesehen, dass dieses Repository als Eclipse-Workspace eingebunden wird. Dazu klickt man auf "File > Switch workspace" und wählt das geklonte Directory aus. Alle Metadaten wurden ebenfalls gepusht, so dass ein Großteil der Konfiguration enthalten ist.
Bitte linkt eure Commits an vorher erstellte Issue-Tickets. Somit können wir nachverfolgen, zu welchem Thema bereits Arbeitsschritte vollzogen wurden. Dazu reicht es beim Commit-Message die ID des Tickets mit einem führend # anzu geben: "#[TICKET-ID] Did some special stuff".
Coming soon...
Zu erstellende Klassen befinden sich im Paket {fpt.com.model}. Hierzu zählen Product, ProductList, Order. Alle erben von den geforderten Interfaces. Die Product-Klasse verwendet zusätzlich bereits Property-Objekte, welche das automatische aktualisieren in den TableViews erlauben.
Die beiden Klassen ProductList und Order erben von ArrayList, da sie lediglich als Container-Objekte für Products dienen und weiter nichts implementieren müssen.
Die ModelShop-Klasse befindet sich ebenfalls im Package {fpt.com.model}. Sie erbt von ModifiableObserableListBase, um als Collection ohne Aufwand in der View verwendet werden zu können. Somit müssen keine lästigen EventListener per Observable registriert werden. JavaFX macht alles voll automatisch. Sobald sich unser Model verändert werden die Views aktualisiert.
Die ViewShop enthält alle notwendigen View-Komponenten (TableView, Buttons, Textfields, etc.) und stellt eine Methode bereit, die es erlaubt einen Listener für die Actions (Button-Klicks) zu registrieren. Da lediglich ein Listener gebunden werden kann erfolgt die Unterscheidung welche Schaltfläche gedrückt wurde über die ID des Buttons.
Die Controller befinden sich im Package {fpt.com.controller}. Die rudimentäre "link()"-Methode wurde durch eine Entkernung verlagert. Die Bindung erfolgt nun über die Implementierung der Interfaces "ModelableController" und "ViewableController". Die Interfaces setzen voraus, dass gewisse Methoden implementiert werden müssen, die einen Verweis auf die benötigte View/Model-Klasse liefern. Der Core-Controller "BaseController" kümmert sich dann um das Instanziieren und das Linken an den Controller.
Die notwendigen View-Elemente werden von ViewShop erstellt und zu Verfügung gestellt. Das Erstellen und Löschen wird per EventListener an den Controller weitergeleitet. Die Registrierung des Listeners erfolgt im ControllerShop::initializeController():
// Set up eventhandler for add and delete Button
view.addEventHandler(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
// Switch between add and delete button
String buttonID = ((Button) event.getSource()).getId();
switch(buttonID) {
case "addButton":
addProduct();
break;
case "deleteButton":
deleteProduct();
break;
case "loadButton":
loadProducts();
break;
case "saveButton":
saveProducts();
break;
}
}
});
Die Prüfung der Eingabe verläuft auf zwei Ebenen. In der ersten Ebene prüft die View, ob alle notwendigen Felder gefüllt sind und aktiviert erst dann den "Add"-Button. Auf der zweiten Ebene erfolgt dann die Validierung der eingegebenen Werte im Controller, also ob "Price" und "Quantity" numerische Werte sind. Der "Delete"-Button wird nur aktiviert, falls eine Zeile in der TableView ausgewählt ist. Ansonsten ist steht die Schaltfläche auf "disabled" und ist nicht klickbar.
Die Tabellen sind in eigenständige Klassen als "TableView" ausgelagert und werden von den Views entsprechend instruiert.
Die GUI wurde erstellt soll laut Blatt aber keinerlei Funktionalitäten enthalten. Die zugehörigen Klassen befinden sich im {fpt.com.controller} und {fpt.com.view}.
Die Klasse zur Generierung von IDs befindet sich im Package {fpt.com.component}. Diese wurde als Singleton implementiert um zu verhindern, dass zugleich mehrere Instanzen existieren. Singleton bedeutet, dass der Constructor auf private gesetzt wird und somit bei einer Instanziierung per "new" eine Exception geworfen wird, da diese als private nicht zugänglich ist. Die Instanz kann nur über die statische Methode "getInstance()" geholt werden. Diese erstellt eine Instanz nur wenn keine existiert:
/**
* Get instance of class
*
* @return
*/
public static IDGenerator getInstance() {
// Check for already existing instance
if (instance == null) {
instance = new IDGenerator();
}
return instance;
}
Beim Speichern eines "Products" wird vom "IDGenerator" eine ID beantragt, welches dem "Product"-Objekt übergeben wird. Dies geschieht in "ControllerShop::addProduct()":
// Set product properties
p.setId(idGen.getId()); // Get an id from IDGenerator and set it as ID of product
p.setName(view.getName());
p.setPrice(price);
p.setQuantity(quantity);
Dieses Handling geschieht im "IDGenerator::getID()", welcher bei jedem ID prüft, ob die maximale Anzahl überschritten wurde oder nicht. Die dazugehörige Exception ist ebenfalls dort als innere Klasse definiert.
/**
* Generate an id
*
* @return
* @throws IDOverflow
*/
public long getId() throws IDOverflow {
if (this.id == IDmax) {
// Throw exception if maximum amount of id is reached
throw new IDOverflow();
}
return id++;
}
Die Auswahl der Serialisierungsstrategie erfolgt über eine ComboBox, welche in der ViewShop initialisiert und gefüllt wird:
comboBox.getItems().addAll(new BinaryStrategy(), new XMLStrategy(), new XStreamStrategy());
Die Strategien implementieren alle die "toString()"-Methode um entsprechend dargestellt zu werden:
@Override
public String toString() {
return "XStreamStrategy";
}
Das Laden/Speichern der Produkte über die jeweilige Strategie erfolgt im ControllerShop, welcher sich über die Listener bindet:
private void loadProducts() {
SerializableStrategy strategy = (SerializableStrategy)this.view.comboBox.getValue();
if (strategy == null) {
Alert.warning("Strategy not selected!", "Please make sure that you select a strategy first.",
"You should choose a strategy first to deserialize your product list.").show();
return;
}
try {
// Clear the products list
model.getProducts().clear();
Product product;
int i = 0;
long lastId = 0;
while ((product = (Product)strategy.readObject()) != null) {
model.doAdd(i++, product);
lastId = product.getId();
}
// Make sure to generate unique IDs
idGen.setId(lastId+1);
Tooltip.disappearingTooltip("Products loaded successfully!");
// Activate save button again
if (i > 0) {
view.saveButton.setDisable(false);
} else {
view.saveButton.setDisable(true);
}
} catch (IOException openError) {
Alert.error("Error",
"Unable to open output stream",
"Please make sure that the resource file is readable and try again.").show();
openError.printStackTrace();
} finally {
try {
strategy.close();
} catch (IOException closeError) {
Alert.error("Error",
"Unable to close output stream",
"Please make sure that the resource file is readable and allready existing on your harddisk.");
closeError.printStackTrace();
}
}
}
private void saveProducts() {
SerializableStrategy strategy = (SerializableStrategy)this.view.comboBox.getValue();
if (strategy == null) {
Alert.warning("Strategy not selected!", "Please make sure that you select a strategy first.",
"You should choose a strategy first to serialize your product list.").show();
return;
}
// Get products
ObservableList<Product> pl = this.getModel().getProducts();
// Serialize each product with the choosen strategy
for (fpt.com.Product p : pl) {
try {
strategy.writeObject(p);
} catch (IOException e) {
Alert.warning("Error: Serializing object", "There was an error while serializing a product with the given strategy.",
"Please make sure that the there are no rights conflicts " +
"and the path is writable."
).show();
e.printStackTrace();
return;
}
}
try {
strategy.close();
} catch (IOException e) {
e.printStackTrace();
}
Tooltip.disappearingTooltip("Products saved successfully!");
}
Das Interface "Serializable" und die "serialVersionUID" kann zu Versionierungszwecken gesetzt sein.
Das Interface wurde implementiert. Ganz klar ist mir aktuell nicht was es genau macht. Anscheinend ist es notwendig um bei der XStream-Serialisierung Mappings vorzunehmen.
Die BinaryStrategy befindet sicht im Package "fpt.com.component.strategy" und implementiert indirekt das geforderte Interface. Da alle Strategien die gleichen Strukturen verwendet, wurden diese in einer Core-Klasse "BaseStrategy" zusammengefasst. Diese kümmert sich um die Streams und alles weitere.
Da diese Klassen nicht das "Serializable"-Interface implementieren können Sie nicht serialisiert werden, auch wenn sie mit der Product-Klasse zusammenhängen. Das gleiche gilt beispielsweise für Threads und Sockets.
Da gibt es nichts zu sagen. KLAPPT!
Der Versuch einer Deserialisierung eines Objektes, bei der sich die Klasse verändert hat birgt verschiedene Probleme.
-
Fall 1: Keine SUID
Falls die Klasse keine SUID hat kommt es bei der kleinsten Änderung der Klasse und der anschließenden Deserialisierung zu einer
Exception: local class incompatible: stream classdesc serialVersionUID = 44259824709362049, local class serialVersionUID = 8962277452270582278
Dies hängt damit zusammen, dass JAVA trotzdem eine SUID generiert und bei der Deserialisierung abgleicht.
-
Fall 2: Eigene SUID
Falls Attribute hinzugekommen sind, werden diese lediglich mit einem Default-Wert belegt (0 oder null). Falls Attribute entfernt werden, werden diese einfach ignoriert . Um zu verhindern, dass alte Objekte deserialisiert werden, kann der Abgleich mit der "serialVersionUID" verwendet werden. Dabei kommt es zu einer InvalidClassException, falls sich die UIDs nicht gleichen und die Deserialisierung wird somit verhindert.
Die "serialVersionUID" kann als Identifikationsnummer der Version einer Klasse verstanden werden. Bei jeder Deserialisierung und dem Versuch des Castings in die Zielklasse geschieht ein Abgleich dieser UID um zu verhindern, dass es zu Kompatibilitätsproblemen kommt.Die Änderung der UID liegt im Ermessen des Entwicklers. Es is sicherlich nicht immer notwendig bei der kleinsten Veränderung der Klasse die UID hochzusetzen.
Durch die Serialisierung und Deserialiserung eines Objektes wird keine komplett eigenständige Kopie im Speicher abgelegt. Somit wird verhindert, dass zwei Variablen auf die gleiche Referenz zu greifen:
public static <T> T deepCopy( T o ) throws Exception
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectOutputStream( baos ).writeObject( o );
ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
Object p = new ObjectInputStream( bais ).readObject();
return (T) p;
}
Die Strategie wurde erstellt und befindet sich im gleichen Package wie die BinaryStrategy. Durch die Verwendung des XML-Decoders/Encoders mussten einige Methoden der BaseStrategy überschrieben werden. Alle zur Serialiserung mit Beans notwendigen Konvention wurden implementiert und betreffen größtenteils das "Product"-Model.
Klappt!
Binäre- und Java-Beans-Serialisierung sind größtenteils gleich. Lediglich die Ablage der serialisierten Objekte ist binär oder im XML-Format. Dieser Unterschied wird durch die Verwendung eines anderen Encoders/Decoders erreicht. Während die binäre Serialisierung ObjectOutputStream/ObjectInputStream verwendet, wrappt die Beans-Serialisierung diese Streams nocheinmal in XMLDecoder/XMLEncoder. Des weiteren muss für die XML-Serialisierung sichergestellt werden, dass einige Konventionen eingehalten werden. Dazu zählt, dass jede Property über zugehörige Getter/Setter-Methoden verfügt. Weiter ist ein No-Argument-Konstruktor notwendig. Für die Sichtbarkeit von bestimmten Properties verwendet Beans nicht das Keyword "transient" sondern den PropertyDescriptor.
- leserlich,
- Deserialisierung,
- Event-Listener "PropertyChangeListener"
- Portabilität
- Konventionspflicht
- Lesbarkeit birgt ein Sicherheitsrisiko
- Keine Konventionspflicht
- Erhöhte Sicherheit durch unlesbares Format
- Nicht lesbar
Attribute können per Annotations oder manuell im XStream-Objekt umbenannt werden:
// Anderer Tag-Name für Property
@XStreamAlias("MySpecialAlias")
private SimpleStringProperty name = new SimpleStringProperty();
Die Konvertierung erfolgt über die "Converter", welche sich im Package "fpt.com.component.converter" befinden. Diese werden dazu verwendet, "SimpleXProperty"-Attribute des Product-Models in lesbare Werte zu übersetzen. Aus einem Objekt wird also ein einfacher primitiver Wert und bei der Deserialisierung umgekehrt. Die Dekoration erfolgt ebenfalls über die Converter. Dies geschieht zum Beispiel für die id, welche mit Nullen aufgefüllt wird.
Diese Strategie ist ebenfalls im Package "fpt.com.component.strategy". Lediglich die"open()"-Methode musste für die Nutzung und Einstellung des XStream-Objektes angepasst werden:
@Override
public void open(InputStream input, OutputStream output) throws IOException {
// Setting the instance with all necessary properties
if (this.xstream == null) {
xstream = new XStream(new DomDriver("UTF-8"));
xstream.registerConverter(
new ExternalizableReflectionConverter(xstream),
XStream.PRIORITY_LOW);
xstream.processAnnotations(fpt.com.model.Product.class);
xstream.alias("Ware", fpt.com.model.Product.class);
xstream.autodetectAnnotations(true);
}
// Setting up object streams for input and output
if (input != null) {
this.is = input;
this.objectIS = xstream.createObjectInputStream(is);
}
if (output != null) {
this.os = output;
this.objectOS = xstream.createObjectOutputStream(output, "Waren");
}
}
Die ID wurde per Annotation (@XStreamAsAttribute) zu einem XML-Attribut transformiert. Das Auffüllen übernimmt der "LongConverter":
/**
* ID
*/
@XStreamAlias("id")
@XStreamConverter(LongConverter.class)
@XStreamAsAttribute
private SimpleLongProperty id = new SimpleLongProperty();
Diese Aufgabe wird vom DoubleConverter übernommen:
/**
* Property to string
*
* @param arg0
* @return
*/
public String toString(Object arg0) {
return String.format(Locale.US,"%3.2f", ((SimpleDoubleProperty)arg0).getValue());
}
Die Converter wurden implementiert und befindet sich im Package "fpt.com.components.converter".