Reputation: 33
I'm writing a simple budgeting program that has a budget class with an array of category classes. Each category class can have child category classes. When I try to save the data to an XML file using JAXB, I get the error com.sun.istack.internal.SAXException2: A cycle is detected in the object graph. This will cause infinitely deep XML
I have searched on the error and see it is caused by a parent child relationship where the parent references the child and the child references the parent. Most answers are to use @XMLTransient.
My problem is that my Category class does not reference either the budget parent nor the category parent if one exists.
I am new to JAXB, but not Java. I'm using this app as a learning experience for JAXB and also JavaFX.
Below are my Budget and Category classes.
package budget.model;
import java.time.LocalDate;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import budget.util.BudgetProperties.DayOfWeek;
import budget.util.LocalDateAdapter;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@XmlRootElement(name = "budget")
public class Budget {
// default to Sunday
ObjectProperty<DayOfWeek> startOfWeek = new SimpleObjectProperty<DayOfWeek>(DayOfWeek.SUNDAY);
ObjectProperty<LocalDate> startDate = new SimpleObjectProperty<LocalDate>();
IntegerProperty daysBeyondWeek = new SimpleIntegerProperty(3);
IntegerProperty numberOfWeeks = new SimpleIntegerProperty();
ObservableList<Category> categories = FXCollections.observableArrayList();
// startOfWeek
public DayOfWeek getStartOfWeek() {
return this.startOfWeek.getValue();
}
public void setStartOfWeek(DayOfWeek startOfWeek) {
this.startOfWeek.set(startOfWeek);
}
public ObjectProperty<DayOfWeek> startOfWeekProperty() {
return this.startOfWeek;
}
// startDate
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getStartDate() {
return this.startDate.getValue();
}
public void setStartDate(LocalDate startDate){
this.startDate.set(startDate);
}
public ObjectProperty<LocalDate> startDateProperty() {
return this.startDate;
}
// daysBeyondWeek
public Integer getDaysBeyondWeek() {
return this.daysBeyondWeek.getValue();
}
public void setDaysBeyondWeek(Integer daysBeyondWeek) {
this.daysBeyondWeek.set(daysBeyondWeek);
}
public IntegerProperty daysBeyondWeekProperty() {
return this.daysBeyondWeek;
}
// numberOFWeeks
public Integer getNumberOfWeeks() {
return this.numberOfWeeks.getValue();
}
public void setNumberOfWeeks(Integer numberOfWeeks) {
this.numberOfWeeks.set(numberOfWeeks);
}
public IntegerProperty numberOfWeeksProperty() {
return numberOfWeeks;
}
// categories
public ObservableList<Category> getCategories () {
return categories;
}
public void setCategories(ObservableList<Category> categories) {
this.categories = categories;
}
public ObservableList<Category> categoriesProperty () {
return categories;
}
}
package budget.model;
import java.time.LocalDate;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import budget.util.BudgetProperties.RepeatFrequency;
import budget.util.LocalDateAdapter;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Category {
StringProperty name = new SimpleStringProperty("");
ObservableList<Category> children = FXCollections.observableArrayList();
StringProperty comments = new SimpleStringProperty("");
ObjectProperty<RepeatFrequency> repeatFrequency = new SimpleObjectProperty<RepeatFrequency>();
ObjectProperty<LocalDate> dueDate = new SimpleObjectProperty<LocalDate>();
DoubleProperty budgetAmount = new SimpleDoubleProperty();
DoubleProperty actualAmount = new SimpleDoubleProperty();
// name
public String getName() {
return name.getValue();
}
public void setName(String name) {
this.name.set(name);
}
public StringProperty nameProperty() {
return name;
}
// children
public ObservableList<Category> getChildren() {
return children;
}
public void setChildren(ObservableList<Category> children) {
this.children.setAll(children);
}
public void addChild(Category category) {
this.children.add(category);
}
// isParent
public Boolean isParent() {
// return this.parent.getValue();
if (children == null || children.isEmpty() || children.size() == 0) {
return false;
} else {
return true;
}
}
// comments
public String getComments() {
return comments.getValue();
}
public void setComments(String comments) {
this.comments.set(comments);
}
public StringProperty commentsProperty() {
return comments;
}
// repeatFrequency
public RepeatFrequency getRepeatFrequency() {
return this.repeatFrequency.getValue();
}
public void setRepeatFrequency(RepeatFrequency repeatFrequency) {
this.repeatFrequency.set(repeatFrequency);
}
public ObjectProperty<RepeatFrequency> repeatFrequencyProperty() {
return this.repeatFrequency;
}
// dueDate
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getDueDate() {
return this.dueDate.getValue();
}
public void setDueDate(LocalDate dueDate) {
this.dueDate.set(dueDate);
}
public ObjectProperty<LocalDate> dueDateProperty() {
return this.dueDate;
}
// budgetAmount
public Double getBudgetAmount() {
return this.budgetAmount.getValue();
}
public void setBudgetAmount(Double budgetAmount) {
this.budgetAmount.set(budgetAmount);
}
public DoubleProperty budgetAmountProperty() {
return this.budgetAmount;
}
// actualAmount
public Double getActualAmount() {
return this.actualAmount.getValue();
}
public void setActualAmount(Double actualAmount) {
this.actualAmount.set(actualAmount);
}
public DoubleProperty actualAmountProperty() {
return this.actualAmount;
}
}
There is another class that handles the marshalling. The function in this class is below
public void saveBudgetData(Budget budget) {
File file = new File(path + BUDGET_FILE);
try {
JAXBContext context = JAXBContext
.newInstance(Budget.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
// Marshalling and saving XML to the file.
m.marshal(budget, file);
} catch (Exception e) { // catches ANY exception
logger.error("exception: ", e);
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Could not save data");
alert.setContentText("Could not save data to file:\n" + file.getPath());
alert.showAndWait();
}
}
I understand that this is a recursive relationship between categories. Is this what it is complaining about? I did not see find this scenario in my searching.
Thanks.
Cleaned up Budget and Category classes
package budget.model;
import java.time.LocalDate;
import java.util.ArrayList;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import budget.util.BudgetProperties.DayOfWeek;
import budget.util.LocalDateAdapter;
@XmlRootElement(name = "budget")
public class BudgetNoFX {
// default to Sunday
DayOfWeek startOfWeek = DayOfWeek.SUNDAY;
// default to now
LocalDate startDate = LocalDate.now();
// number of days beyond week end to include in list of due bills
// default to 3
Integer daysBeyondWeek = new Integer(3);
Integer numberOfWeeks = new Integer(0);
ArrayList<CategoryNoFX> categories = new ArrayList<CategoryNoFX>();
// startOfWeek
public DayOfWeek getStartOfWeek() {
return this.startOfWeek;
}
public void setStartOfWeek(DayOfWeek startOfWeek) {
this.startOfWeek = startOfWeek;
}
// startDate
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getStartDate() {
return this.startDate;
}
public void setStartDate(LocalDate startDate){
this.startDate = startDate;
}
// daysBeyondWeek
public Integer getDaysBeyondWeek() {
return this.daysBeyondWeek;
}
public void setDaysBeyondWeek(Integer daysBeyondWeek) {
this.daysBeyondWeek = daysBeyondWeek;
}
// numberOFWeeks
public Integer getNumberOfWeeks() {
return this.numberOfWeeks;
}
public void setNumberOfWeeks(Integer numberOfWeeks) {
this.numberOfWeeks = numberOfWeeks;
}
// categories
public ArrayList<CategoryNoFX> getCategories () {
return categories;
}
public void setCategories(ArrayList<CategoryNoFX> categories) {
this.categories = categories;
}
}
package budget.model;
import java.time.LocalDate;
import java.util.ArrayList;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import budget.util.BudgetProperties.RepeatFrequency;
import budget.util.LocalDateAdapter;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class CategoryNoFX {
public static final String ROOT_CATEGORY = "ROOT";
String name = new String("");
ArrayList<CategoryNoFX> children = new ArrayList<CategoryNoFX>();
String comments = new String("");
// default to monthly
RepeatFrequency repeatFrequency = RepeatFrequency.MONTHLY;
LocalDate dueDate = LocalDate.now();
Double budgetAmount = new Double(0);
Double actualAmount = new Double(0);
// name
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// children
public ArrayList<CategoryNoFX> getChildren() {
return children;
}
public void setChildren(ArrayList<CategoryNoFX> children) {
this.children = children;
}
public void addChild(CategoryNoFX category) {
this.children.add(category);
}
// isParent
public Boolean isParent() {
if (children == null || children.isEmpty() || children.size() == 0) {
return false;
} else {
return true;
}
}
// comments
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
// repeatFrequency
public RepeatFrequency getRepeatFrequency() {
return this.repeatFrequency;
}
public void setRepeatFrequency(RepeatFrequency repeatFrequency) {
this.repeatFrequency = repeatFrequency;
}
// dueDate
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getDueDate() {
return this.dueDate;
}
public void setDueDate(LocalDate dueDate) {
this.dueDate = dueDate;
}
// budgetAmount
public Double getBudgetAmount() {
return this.budgetAmount;
}
public void setBudgetAmount(Double budgetAmount) {
this.budgetAmount = budgetAmount;
}
// actualAmount
public Double getActualAmount() {
return this.actualAmount;
}
public void setActualAmount(Double actualAmount) {
this.actualAmount = actualAmount;
}
}
I updated the saveBudgetData function to use the new budget class
public void saveBudgetData(BudgetNoFX budget) {
File file = new File(path + BUDGET_FILE);
try {
JAXBContext context = JAXBContext
.newInstance(BudgetNoFX.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
// Marshalling and saving XML to the file.
m.marshal(budget, file);
} catch (Exception e) { // catches ANY exception
logger.error("exception: ", e);
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Could not save data");
alert.setContentText("Could not save data to file:\n" + file.getPath());
alert.showAndWait();
}
}
Upvotes: 0
Views: 493
Reputation: 33
I'm a bit embarrassed. I know you have to be careful with recursion and that was my problem.
Before building the ui, I hardcoded some values - created a budget and added some categories. I should have posted that code. I had set one of the categories as a child to itself.
Category food = new Category();
food.setName("Food");
categories.add(food);
Category groceries = new Category();
groceries.setBudgetAmount(new Double(120));
groceries.setName("Groceries");
// groceries.setParentCategory("Food");
groceries.setRepeatFrequency(RepeatFrequency.WEEKLY);
food.addChild(food); <-- problem line
Once I fixed the offending line to
food.addChild(groceries);
it started working.
I found it by by commenting out the save funciton to XML and instead wrote out my budget object to the screen.
I had recently read this tutorial: http://code.makery.ch/library/javafx-8-tutorial/ and built another simple app. This is where the LocalDateAdapter class came from. In part 5, he explains about jaxb and lists. I've made some code changes to better handle my lists and I'm getting xml output that I'm happy with.
Thanks for taking the time to look at my code and help me out. If I ever get this done, maybe I'll post the app/code to the Internet. I've never done that before and don't know the best place though.
Again, thanks. Chris
Upvotes: 1