Creating mocks for unit test - retrofit - java

There is commandline app to show tomorrow's forecast using a public API
Sample output could be as follows:
Tomorrow (2019/05/01) in city XYZ:
Clear
Temp: 26.5 °C
Wind: 7.6 mph
Humidity: 61%
Question : How will you create a test case such that tests should not touch the real service and work without the Internet.
I tried creating the junit test for the same and its working fine till the time i am using the api directly.
Can someone please help how can i create a mock for my unit testing.
App.java
import api.ForecastServiceImpl;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.io.IOException;
import java.time.LocalDate;
public class App {
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Pass city name as an argument");
System.exit(1);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://www.metaweather.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
LocalDate tomorrow = LocalDate.now().plusDays(1);
ForecastServiceImpl service = new ForecastServiceImpl(retrofit);
System.out.println(service.getForecast(args[0], tomorrow));
}
}
ForecastServiceImpl.java
package api;
import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.Retrofit;
import util.PathDate;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
public class ForecastServiceImpl {
private Retrofit retrofit;
public ForecastServiceImpl(Retrofit retrofit) {
this.retrofit = retrofit;
}
public String getForecast(String cityName, LocalDate date) throws IOException {
PathDate pathDate = new PathDate(date);
ForecastService service = retrofit.create(ForecastService.class);
Call<List<City>> findCityCall = service.findCityByName(cityName.toLowerCase());
City city = Objects.requireNonNull(findCityCall.execute().body())
.stream()
.findFirst()
.orElseThrow(() -> new RuntimeException(String.format("Can't find city id for %s", cityName)));
Call<List<Forecast>> forecastCall = service.getForecast(city.getWoeid(), pathDate);
Forecast forecast = Objects.requireNonNull(forecastCall.execute().body())
.stream()
.findFirst()
.orElseThrow(() -> new RuntimeException(String.format("Can't get forecast for %s", cityName)));
return String.format("Weather on (%s) in %s:\n%s", pathDate, city.getTitle(), forecast);
}
}
ForecastService.java
package api;
import model.City;
import model.Forecast;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
import retrofit2.http.Query;
import util.PathDate;
import java.util.List;
public interface ForecastService {
#GET("/api/location/{city_id}/{date}/")
Call<List<Forecast>> getForecast(#Path("city_id") Long cityId, #Path("date") PathDate date);
#GET("/api/location/search/")
Call<List<City>> findCityByName(#Query("query") String city);
}
City.java
package model;
public class City {
private String title;
private Long woeid;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Long getWoeid() {
return woeid;
}
public void setWoeid(Long woeid) {
this.woeid = woeid;
}
}
Forecast.java
package model;
import com.google.gson.annotations.SerializedName;
public class Forecast {
private Long id;
#SerializedName("weather_state_name")
private String weatherState;
#SerializedName("wind_speed")
private Double windSpeed;
#SerializedName("the_temp")
private Double temperature;
private Integer humidity;
public Long getId() {
return id;
}
public Forecast setId(Long id) {
this.id = id;
return this;
}
public String getWeatherState() {
return weatherState;
}
public Forecast setWeatherState(String weatherState) {
this.weatherState = weatherState;
return this;
}
public Double getWindSpeed() {
return windSpeed;
}
public Forecast setWindSpeed(Double windSpeed) {
this.windSpeed = windSpeed;
return this;
}
public Double getTemperature() {
return temperature;
}
public Forecast setTemperature(Double temperature) {
this.temperature = temperature;
return this;
}
public Integer getHumidity() {
return humidity;
}
public Forecast setHumidity(Integer humidity) {
this.humidity = humidity;
return this;
}
#Override
public String toString() {
return String.format("%s\nTemp: %.1f °C\nWind: %.1f mph\nHumidity: %d%%",
weatherState, temperature, windSpeed, humidity);
}
}
PathDate.java
package util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class PathDate {
private final LocalDate date;
public PathDate(LocalDate date) {
this.date = date;
}
#Override public String toString() {
return date.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
}
}
Utils.java
package util;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Utils {
public static byte[] readResourceFileToBytes(String filename) {
byte[] fileBytes = new byte[0];
try {
Path path = Paths.get(Utils.class.getClassLoader().getResource(filename).toURI());
fileBytes = Files.readAllBytes(path);
} catch (URISyntaxException|IOException|NullPointerException e) {
e.printStackTrace();
}
return fileBytes;
}
}

The service.getForecast(city.getWoeid(), pathDate); returns us a Call<List<Forecast>> object. And when we call execute on this object then an actual API call is made. Since we don't want to do an actual API call, we can try mocking the Call<List<Forecast>> object.
We can mock the Call class like
Call<List<Forecast>> mockedListForeCast = mock(Call.class);
The above statement creates a mock object of Call<List<Forecast>>. We can use when to define what should happen when a method is called on the mocked object.
// here I am returning the singleton list, you can return a list of forecast
when(mockedListForeCast.execute()).thenReturn(Response.success(Collections.singletonList()));
The above line says that return an empty list of forecast when the execute function was called on the mocked object.
This way we are mocking the API response and we don't have to do an actual API call.
Edit:
You can also mock your retrofit API using Retrofit Mock also.

Related

Consuming JSON that may return a different data

I'd like to write some java code using spring boot to consume JSON data from a specific endpoint. However with each request the response may return different data fields as such.
{"success":true,"terms":"https:\/\/coinlayer.com\/terms","privacy":"https:\/\/coinlayer.com\/privacy","timestamp":1645616586,"target":"USD","rates":{"BTC":39049.424242}}
{"success":true,"terms":"https:\/\/coinlayer.com\/terms","privacy":"https:\/\/coinlayer.com\/privacy","timestamp":1645626666,"target":"USD","rates":{"BTC":39061.184046,"ETH":2726.545731}}
{"success":true,"terms":"https:\/\/coinlayer.com\/terms","privacy":"https:\/\/coinlayer.com\/privacy","timestamp":1645626966,"target":"USD","rates":{"ADA":0.939301,"BTC":39006.990707,"ETH":2720.502765}}
and so on.
Below is my current code which deals with the first case presented. I could write another Rates.java to cater for the second case and so on but I'm looking to have one Rates.java file which deals with all possible cases.
LiveData.java
package com.example.consumingrest;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties(ignoreUnknown = true)
public class LiveData {
private Boolean success;
private String terms;
private String privacy;
private Long timestamp;
private String target;
private Rates rates;
public LiveData() {
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public String getTerms() {
return terms;
}
public void setTerms(String terms) {
this.terms = terms;
}
public String getPrivacy() {
return privacy;
}
public void setPrivacy(String privacy) {
this.privacy = privacy;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public Rates getRates() {
return rates;
}
public void setValue(Rates rates) {
this.rates = rates;
}
#Override
public String toString() {
return "LiveData{" +
"success='" + success + '\'' +
"terms='" + terms + '\'' +
"privacy='" + privacy + '\'' +
"timestamp='" + timestamp + '\'' +
"target='" + target + '\'' +
"rates=" + rates +
'}';
}
}
Rates.java
package com.example.consumingrest;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
#JsonIgnoreProperties(ignoreUnknown = true)
public class Rates {
#JsonProperty(value = "BTC")
private BigDecimal btc;
public Rates() {
}
public BigDecimal getBTC() {
return this.btc;
}
public void setId(BigDecimal btc) {
this.btc = btc;
}
#Override
public String toString() {
return "{" +
"BTC='" + btc + '\''+
'}';
}
}
ConsumingRest.java (main)
package com.example.consumingrest;
import java.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
#SpringBootApplication
public class ConsumingRestApplication {
private static final Logger log = LoggerFactory.getLogger(ConsumingRestApplication.class);
public static void main(String[] args) {
SpringApplication.run(ConsumingRestApplication.class, args);
}
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
#Bean
public CommandLineRunner run(RestTemplate restTemplate) throws Exception {
return args -> {
LiveData liveData = restTemplate.getForObject(
"http://api.coinlayer.com/api/live?access_key=121a4df8b95fd5be872da3bad101cd73&target=EUR&symbols=BTC", LiveData.class);
log.info(liveData.toString());
};
}
}
As mentioned in the comments, seems you want a map containing the rates:
#JsonIgnoreProperties(ignoreUnknown = true)
public class LiveData {
...
private Map<String, BigDecimal> rates;
See Mapping a Dynamic JSON Object
Any Json Object can be parsed to Map<String, Object> where Object may be anything including Map or List. So, your map may be nested with any depth and it can contain Lists with any objects including maps. Any of your responses can be always parsed to that structure. So, return that structure and you won't have to worry about different formats - This is one size fits all.

How I can to validate my Junit test with Gson parse

I'm using the Gson library and jakarta. Although I have been able to use the conversion in CarrinhoResource.java as below, my ClienteTest.java cannot use the String content (already in json) inside the cart. I cant run my test a just only message into my intellij is (Cannot resolve method 'fromJson(java.lang.String)').
Can someone help me?
Class CarrinhoResource.java
package br.com.alura.loja.resource;
import br.com.alura.loja.dao.CarrinhoDAO;
import br.com.alura.loja.modelo.Carrinho;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
#Path("/v1/carrinhos")
public class CarrinhoResource {
#GET
#Produces(MediaType.APPLICATION_JSON)
public String busca(){
Carrinho carrinho = new CarrinhoDAO().busca(1L);
return carrinho.toJson();
}
}
Carrinho.java
package br.com.alura.loja.modelo;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.google.gson.Gson;
public class Carrinho {
private List<Produto> produtos = new ArrayList<Produto>();
private String rua;
private String cidade;
private long id;
public Carrinho adiciona(Produto produto) {
produtos.add(produto);
return this;
}
public Carrinho para(String rua, String cidade) {
this.rua = rua;
this.cidade = cidade;
return this;
}
public Carrinho setId(long id) {
this.id = id;
return this;
}
public String getRua() {
return rua;
}
public void setRua(String rua) {
this.rua = rua;
}
public void setCidade(String cidade) {
this.cidade = cidade;
}
public long getId() {
return id;
}
public void remove(long id) {
for (Iterator iterator = produtos.iterator(); iterator.hasNext();) {
Produto produto = (Produto) iterator.next();
if(produto.getId() == id) {
iterator.remove();
}
}
}
public void troca(Produto produto) {
remove(produto.getId());
adiciona(produto);
}
public void trocaQuantidade(Produto produto) {
for (Iterator iterator = produtos.iterator(); iterator.hasNext();) {
Produto p = (Produto) iterator.next();
if(p.getId() == produto.getId()) {
p.setQuantidade(produto.getQuantidade());
return;
}
}
}
public List<Produto> getProdutos() {
return produtos;
}
public String toJson() {
return new Gson().toJson(this);
}
}
ClienteTest.java
package br.com.alura.loja;
import br.com.alura.loja.modelo.Carrinho;
import com.google.gson.*;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;
import org.junit.Assert;
import org.junit.Test;
public class ClienteTest {
#Test
public void testaConexaoServidor() {
Client client = ClientBuilder.newClient();
WebTarget target = client.target("http://localhost:8085");
String conteudo = target.path("/v1/carrinhos").request().get(String.class);
Carrinho carrinho = (Carrinho) new Gson().fromJson(conteudo); **//Cannot resolve method 'fromJson(java.lang.String)'/**
System.out.println(carrinho);
Assert.assertEquals("Rua Vergueiro, 3185", carrinho.getRua());
}
}
Carrinho carrinho = (Carrinho) new Gson().fromJson(conteudo); **//Cannot resolve method 'fromJson(java.lang.String)'/**
The reason for this is that there is no Gson.fromJson(String) method, see the Gson class documentation. For deserialization Gson needs to know which type you are expecting, so all fromJson methods have a second parameter representing the type.
You can simply change your code to:
Carrinho carrinho = new Gson().fromJson(conteudo, Carrinho.class);

Why can't the database still not save the data with my current TypeConverter?

I am stuck with implementing a TypeConverter to my Database. I have added the TypeConverters but it still keeps saying that it cannot figure out how to save the field into the database. Or maybe I have missed something? I was following this article to create TypeConverters (https://android.jlelse.eu/room-persistence-library-typeconverters-and-database-migration-3a7d68837d6c), which is with my knowledge so far a bit hard to understand.
Any help would be appreciated!
MyGame.java:
package com.riceplant.capstoneproject.room;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.riceplant.capstoneproject.data.Cover;
import com.riceplant.capstoneproject.data.Genre;
import com.riceplant.capstoneproject.data.Platform;
import com.riceplant.capstoneproject.data.ReleaseDate;
import com.riceplant.capstoneproject.data.Video;
import java.util.List;
#Entity(tableName = "game")
public class MyGame {
#PrimaryKey(autoGenerate = true)
private Integer mId;
private Cover mCover;
private String mName;
private Double mPopularity;
private String mSummary;
private List<Genre> mGenres;
private List<Platform> mPlatform;
private Double mRating;
private List<ReleaseDate> mReleaseDate;
private List<Video> mVideos;
#Ignore
public MyGame() {
}
public MyGame(Integer id,
Cover cover,
String name,
Double popularity,
String summary,
List<Genre> genres,
List<Platform> platform,
Double rating,
List<ReleaseDate> releaseDate,
List<Video> videos) {
mId = id;
mCover = cover;
mName = name;
mPopularity = popularity;
mSummary = summary;
mGenres = genres;
mPlatform = platform;
mRating = rating;
mReleaseDate = releaseDate;
mVideos = videos;
}
public Integer getId() {
return mId;
}
public void setId(Integer id) {
id = mId;
}
public Cover getCover() {
return mCover;
}
public void setCover(Cover cover) {
cover = mCover;
}
public String getName() {
return mName;
}
public void setName(String name) {
name = mName;
}
public Double getPopularity() {
return mPopularity;
}
public void setPopularity(Double popularity) {
popularity = mPopularity;
}
public String getSummary() {
return mSummary;
}
public void setSummary(String summary) {
summary = mSummary;
}
public List<Genre> getGenres() {
return mGenres;
}
public void setGenres(List<Genre> genres) {
genres = mGenres;
}
public List<Platform> getPlatform() {
return mPlatform;
}
public void setPlatform(List<Platform> platform) {
platform = mPlatform;
}
public Double getRating() {
return mRating;
}
public void setRating(Double rating) {
rating = mRating;
}
public List<ReleaseDate> getReleaseDate() {
return mReleaseDate;
}
public void setReleaseDate(List<ReleaseDate> releaseDate) {
releaseDate = mReleaseDate;
}
public List<Video> getVideos() {
return mVideos;
}
public void setVideos(List<Video> videos) {
videos = mVideos;
}
}
Converters.java
package com.riceplant.capstoneproject.room;
import androidx.room.TypeConverter;
import com.riceplant.capstoneproject.data.Cover;
import com.riceplant.capstoneproject.data.Genre;
import com.riceplant.capstoneproject.data.Platform;
import com.riceplant.capstoneproject.data.Video;
public class Converters {
#TypeConverter
public static Cover toCover(String value) {
return value == null ? null : new Cover();
}
#TypeConverter
public static String toString(Cover value) {
return value == null ? null : value.getUrl();
}
#TypeConverter
public static Genre toGenre(String value) {
return value == null ? null : new Genre();
}
#TypeConverter
public static String toString(Genre value) {
return value == null ? null : value.getName();
}
#TypeConverter
public static Platform toPlatform(String value) {
return value == null ? null : new Platform();
}
#TypeConverter
public static String toString(Platform value) {
return value == null ? null : value.getName();
}
#TypeConverter
public static Video toString(String value) {
return value == null ? null : new Video();
}
#TypeConverter
public static String toVideo(Video value) {
return value == null ? null : value.getVideoId();
}
}
GameRoomDatabase.java
package com.riceplant.capstoneproject.room;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
#Database(entities = {MyGame.class}, version = 3, exportSchema = false)
#TypeConverters({Converters.class})
public abstract class GameRoomDatabase extends RoomDatabase {
private static final String LOG_TAG = GameRoomDatabase.class.getSimpleName();
private static final Object LOCK = new Object();
private static final String DATABASE_NAME = "gameslist";
private static GameRoomDatabase sInstance;
public static GameRoomDatabase getInstance(Context context) {
if (sInstance == null) {
synchronized (LOCK) {
sInstance = Room.databaseBuilder(context.getApplicationContext(),
GameRoomDatabase.class, GameRoomDatabase.DATABASE_NAME)
.fallbackToDestructiveMigration()
.build();
}
}
return sInstance;
}
public abstract GameDao gameDao();
}
GameDao.java
package com.riceplant.capstoneproject.room;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
#Dao
public interface GameDao {
#Query("SELECT * FROM game ORDER BY mId")
LiveData<List<MyGame>> loadAllGames();
#Insert
void insertGame(MyGame myGame);
#Update(onConflict = OnConflictStrategy.REPLACE)
void updateGame(MyGame myGame);
#Delete
void deleteGame(MyGame myGame);
#Query("SELECT * FROM game WHERE mId = :id")
MyGame loadGameById(int id);
}
GameViewModel
package com.riceplant.capstoneproject.room;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
public class GameViewModel extends AndroidViewModel {
private LiveData<List<MyGame>> games;
public GameViewModel(#NonNull Application application) {
super(application);
GameRoomDatabase database = GameRoomDatabase.getInstance(this.getApplication());
games = database.gameDao().loadAllGames();
}
public LiveData<List<MyGame>> getGames() {
return games;
}
}
Your DB contains Lists of Genre, Platform, ReleaseDate and Video. SQLite supports column types of INTEGER, REAL, TEXT and BLOB. You must provide methods for conversion of your List types to/from String(TEXT) or one of the other supported SQLite types.
For example:
#TypeConverter
public static List<Genre> toGenreList(String value) {
// TODO conversion code
}
#TypeConverter
public static String toString(List<Genre> value) {
// TODO conversion code
}

Java/Spring - Object references an unsaved transient instance

Well, my issue is related to when I try to save an entity (Parade) which has a collection of other entity (AcmeFloat) using the corresponding default CRUD method in convenient repository (the code is attached below). When it reaches the save() method, it throws an exception.
I tried to save the pertinent entities of AcmeFloat class which need to be updated by hand, but, whatever I do (whether save first the Parade updated and later update and save each AcmeFloat or inside out) raises an exception.
So I went into Stack Overflow and the solution I found is putting a 'cascade=CascadeType.ALL' inside the #ManyToMany anotation, without success. I tried also to do both things, which also fails.
Here is the code:
Parade (Domain Class):
package domain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.Valid;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.format.annotation.DateTimeFormat;
#Entity
#Access(AccessType.PROPERTY)
public class Parade extends DomainEntity {
// Fields -----------------------------------------------------------------
private String title;
private String description;
private Date moment;
private String ticker;
private boolean isDraft;
// Relationships ----------------------------------------------------------
private Brotherhood brotherhood;
private Collection<AcmeFloat> acmeFloats;
// Field access methods ---------------------------------------------------
#NotBlank
public String getTitle() {
return this.title;
}
public void setTitle(final String title) {
this.title = title;
}
#NotBlank
public String getDescription() {
return this.description;
}
public void setDescription(final String description) {
this.description = description;
}
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern = "dd/MM/yyyy HH:mm")
public Date getMoment() {
return this.moment;
}
public void setMoment(final Date moment) {
this.moment = moment;
}
#NotBlank
#Pattern(regexp = "^([\\d]){6}-([A-Z]){5}$")
#Column(unique = true)
public String getTicker() {
return this.ticker;
}
public void setTicker(final String ticker) {
this.ticker = ticker;
}
#Basic
public boolean getIsDraft() {
return this.isDraft;
}
public void setIsDraft(final boolean isDraft) {
this.isDraft = isDraft;
}
// Relationship access methods --------------------------------------------
#Valid
#ManyToOne(optional = true)
public Brotherhood getBrotherhood() {
return this.brotherhood;
}
public void setBrotherhood(final Brotherhood brotherhood) {
this.brotherhood = brotherhood;
}
#Valid
#ManyToMany(mappedBy = "parades", cascade = CascadeType.ALL)
public Collection<AcmeFloat> getAcmeFloats() {
return new ArrayList<AcmeFloat>(this.acmeFloats);
}
public void setAcmeFloats(final Collection<AcmeFloat> acmeFloats) {
this.acmeFloats = acmeFloats;
}
}
AcmeFloat (Domain Class):
package domain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
#Entity
#Access(AccessType.PROPERTY)
public class AcmeFloat extends DomainEntity {
// Fields -----------------------------------------------------------------
private String title;
private String description;
private List<String> pictures;
// Relationships ----------------------------------------------------------
private Collection<Parade> parades;
private Brotherhood brotherhood;
// Field access methods ---------------------------------------------------
#NotNull
#NotBlank
public String getTitle() {
return this.title;
}
public void setTitle(final String title) {
this.title = title;
}
#NotNull
#NotBlank
public String getDescription() {
return this.description;
}
public void setDescription(final String description) {
this.description = description;
}
//Optional
//#URL
#NotNull
#ElementCollection
public List<String> getPictures() {
return this.pictures;
}
public void setPictures(final List<String> pictures) {
this.pictures = pictures;
}
// Relationship access methods --------------------------------------------
#ManyToMany(cascade = CascadeType.ALL)
#Valid
public Collection<Parade> getParades() {
return this.parades;
}
public void setParades(final Collection<Parade> parades) {
this.parades = new ArrayList<Parade>(parades);
}
#ManyToOne
#Valid
public Brotherhood getBrotherhood() {
return this.brotherhood;
}
public void setBrotherhood(final Brotherhood brotherhood) {
this.brotherhood = brotherhood;
}
}
ParadeForm (Form Object):
package forms;
import java.util.Collection;
import java.util.Date;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;
import domain.AcmeFloat;
public class ParadeForm {
// Fields -----------------------------------------------------------------
private int id;
private String title;
private String description;
private Date moment;
// Relationships ----------------------------------------------------------
private Collection<AcmeFloat> acmeFloats;
// Field access methods ---------------------------------------------------
#Range(min = 0)
public int getId() {
return this.id;
}
public void setId(final int id) {
this.id = id;
}
#NotBlank
public String getTitle() {
return this.title;
}
public void setTitle(final String title) {
this.title = title;
}
#NotBlank
public String getDescription() {
return this.description;
}
public void setDescription(final String description) {
this.description = description;
}
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(pattern = "dd/MM/yyyy HH:mm")
public Date getMoment() {
return this.moment;
}
public void setMoment(final Date moment) {
this.moment = moment;
}
// Relationship access methods --------------------------------------------
#NotNull
public Collection<AcmeFloat> getAcmeFloats() {
return this.acmeFloats;
}
public void setAcmeFloats(final Collection<AcmeFloat> acmeFloats) {
this.acmeFloats = acmeFloats;
}
}
ParadeRepository:
package repositories;
import java.util.Date;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import domain.Parade;
#Repository
public interface ParadeRepository extends JpaRepository<Parade, Integer> {
#Query("select p from Parade p where p.ticker like ?1")
List<Parade> findByTicker(String ticker);
#Query("select p from Parade p where p.moment < ?1 and p.isDraft = false")
List<Parade> findBeforeDate(Date date);
#Query("select p from Parade p where p.isDraft = false")
List<Parade> findAllFinal();
#Query("select p from Parade p where p.isDraft = false and brotherhood.userAccount.id = ?1")
List<Parade> findAllFinalByBrotherhoodAccountId(int id);
#Query("select p from Parade p where brotherhood.userAccount.id = ?1")
List<Parade> findAllByBrotherhoodAccountId(int id);
#Query("select p from Parade p join p.brotherhood.enrolments e where e.member.id= ?1")
List<Parade> findPossibleMemberParades(int id);
}
AcmeFloatRepository:
package repositories;
import java.util.Collection;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import domain.AcmeFloat;
#Repository
public interface AcmeFloatRepository extends JpaRepository<AcmeFloat, Integer> {
#Query("select f from AcmeFloat f where f.brotherhood.userAccount.id = ?1")
Collection<AcmeFloat> findAcmeFloats(int principalId);
}
ParadeService:
package services;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Random;
import javax.validation.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import repositories.ParadeRepository;
import security.LoginService;
import domain.AcmeFloat;
import domain.Parade;
import forms.ParadeForm;
#Service
#Transactional
public class ParadeService {
////////////////////////////////////////////////////////////////////////////////
// Managed repository
#Autowired
private ParadeRepository paradeRepository;
////////////////////////////////////////////////////////////////////////////////
// Supporting services
#Autowired
private BrotherhoodService brotherhoodService;
////////////////////////////////////////////////////////////////////////////////
// Supporting services
#Autowired
private Validator validator;
////////////////////////////////////////////////////////////////////////////////
// Ticker generation fields
private static final String TICKER_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final int TICKER_LENGTH = 5;
private final Random random = new Random();
////////////////////////////////////////////////////////////////////////////////
// Constructors
public ParadeService() {
super();
}
////////////////////////////////////////////////////////////////////////////////
// CRUD methods
public Parade create() {
final Parade parade = new Parade();
parade.setBrotherhood(this.brotherhoodService.findByUserAccountId(LoginService.getPrincipal().getId()));
parade.setAcmeFloats(new ArrayList<AcmeFloat>());
parade.setIsDraft(true);
parade.setDescription("");
parade.setTitle("");
if (parade.getTicker() == null || parade.getTicker().isEmpty()) {
final Calendar calendar = new GregorianCalendar();
String dateString = "";
dateString += String.format("%02d", calendar.get(Calendar.YEAR) % 100);
dateString += String.format("%02d", calendar.get(Calendar.MONTH) + 1);
dateString += String.format("%02d", calendar.get(Calendar.DAY_OF_MONTH));
dateString += "-";
String ticker;
do {
ticker = dateString;
for (int i = 0; i < ParadeService.TICKER_LENGTH; ++i)
ticker += ParadeService.TICKER_ALPHABET.charAt(this.random.nextInt(ParadeService.TICKER_ALPHABET.length()));
} while (this.paradeRepository.findByTicker(ticker).size() > 0);
parade.setTicker(ticker);
}
return parade;
}
public Parade save(final Parade parade) {
Assert.notNull(parade);
//final Parade originalParade = this.paradeRepository.findOne(parade.getId());
//if (originalParade != null)
Assert.isTrue(parade.getIsDraft());
Assert.isTrue(parade.getMoment().after(new Date()));
//TODO: if ticker existe en BBDD, generar nuevo, else, se guarda
return this.paradeRepository.save(parade);
}
public void delete(final Parade parade) {
Assert.notNull(parade);
Assert.isTrue(parade.getIsDraft());
this.paradeRepository.delete(parade);
}
public Parade findOne(final int id) {
return this.paradeRepository.findOne(id);
}
public List<Parade> findAll() {
return this.paradeRepository.findAll();
}
////////////////////////////////////////////////////////////////////////////////
// Ancillary methods
public List<Parade> findWithin30Days() {
final Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DATE, 30);
final Date plus30Days = calendar.getTime();
return this.paradeRepository.findBeforeDate(plus30Days);
}
public List<Parade> findAllByBrotherhoodAccountId(final int id) {
return this.paradeRepository.findAllByBrotherhoodAccountId(id);
}
public List<Parade> findAllFinalByBrotherhoodAccountId(final int id) {
return this.paradeRepository.findAllFinalByBrotherhoodAccountId(id);
}
public List<Parade> findAllFinal() {
return this.paradeRepository.findAllFinal();
}
public List<Parade> findPossibleMemberParades(final int memberId) {
return this.paradeRepository.findPossibleMemberParades(memberId);
}
public Parade reconstruct(final ParadeForm paradeForm, final BindingResult binding) {
Parade result;
if (paradeForm.getId() == 0)
result = this.create();
else
result = this.paradeRepository.findOne(paradeForm.getId());
result.setTitle(paradeForm.getTitle());
result.setDescription(paradeForm.getDescription());
result.setMoment(paradeForm.getMoment());
result.setAcmeFloats(paradeForm.getAcmeFloats());
this.validator.validate(result, binding);
this.paradeRepository.flush();
if (binding.hasErrors())
throw new ValidationException();
return result;
}
}
AcmeFloatService:
package services;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import repositories.AcmeFloatRepository;
import domain.AcmeFloat;
import domain.Parade;
#Service
#Transactional
public class AcmeFloatService {
////////////////////////////////////////////////////////////////////////////////
// Managed repository
#Autowired
private AcmeFloatRepository acmeFloatRepository;
////////////////////////////////////////////////////////////////////////////////
// Supporting services
////////////////////////////////////////////////////////////////////////////////
// Constructors
public AcmeFloatService() {
super();
}
////////////////////////////////////////////////////////////////////////////////
// CRUD methods
public AcmeFloat create() {
final AcmeFloat result = new AcmeFloat();
// set fields
result.setTitle("");
result.setDescription("");
result.setPictures(new ArrayList<String>());
// set relationships
result.setParades(new ArrayList<Parade>());
result.setBrotherhood(null);
return result;
}
public AcmeFloat save(final AcmeFloat acmeFloat) {
Assert.isTrue(acmeFloat != null);
return this.acmeFloatRepository.save(acmeFloat);
}
public Iterable<AcmeFloat> save(final Iterable<AcmeFloat> acmeFloats) {
Assert.isTrue(acmeFloats != null);
return this.acmeFloatRepository.save(acmeFloats);
}
public void delete(final AcmeFloat acmeFloat) {
Assert.isTrue(acmeFloat != null);
this.acmeFloatRepository.delete(acmeFloat);
}
public void delete(final Iterable<AcmeFloat> acmeFloats) {
Assert.isTrue(acmeFloats != null);
this.acmeFloatRepository.delete(acmeFloats);
}
public AcmeFloat findOne(final int id) {
return this.acmeFloatRepository.findOne(id);
}
public List<AcmeFloat> findAll() {
return this.acmeFloatRepository.findAll();
}
////////////////////////////////////////////////////////////////////////////////
// Ancillary methods
public Collection<AcmeFloat> findAcmeFloats(final int id) {
return this.acmeFloatRepository.findAcmeFloats(id);
}
}
ParadeController:
package controllers;
import java.util.Collection;
import javax.validation.Valid;
import javax.validation.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import security.LoginService;
import security.UserAccount;
import services.AcmeFloatService;
import services.BrotherhoodService;
import services.ParadeService;
import domain.AcmeFloat;
import domain.Brotherhood;
import domain.Parade;
import forms.ParadeForm;
#Controller
#RequestMapping("/parade")
public class ParadeController extends AbstractController {
// Services ---------------------------------------------------------------
#Autowired
private ParadeService paradeService;
#Autowired
private BrotherhoodService brotherhoodService;
#Autowired
private AcmeFloatService acmeFloatService;
// Constructors -----------------------------------------------------------
public ParadeController() {
}
// List -------------------------------------------------------------------
#RequestMapping(value = "/brotherhood/list", method = RequestMethod.GET)
public ModelAndView list() {
final ModelAndView result;
Collection<Parade> parades;
parades = this.paradeService.findAllByBrotherhoodAccountId(LoginService.getPrincipal().getId());
result = new ModelAndView("parade/brotherhood/list");
result.addObject("parades", parades);
result.addObject("requestURI", "parade/brotherhood/list.do");
return result;
}
// Create -----------------------------------------------------------------
#RequestMapping(value = "/brotherhood/create", method = RequestMethod.GET)
public ModelAndView create() {
final ModelAndView result;
Parade parade;
parade = this.paradeService.create();
parade.setIsDraft(true);
result = this.createEditModelAndView(parade, "create");
return result;
}
// Edit -------------------------------------------------------------------
#RequestMapping(value = "/brotherhood/edit", method = RequestMethod.GET)
public ModelAndView edit(#RequestParam final int paradeId) {
ModelAndView result;
Parade parade;
parade = this.paradeService.findOne(paradeId);
Assert.notNull(parade);
result = this.createEditModelAndView(parade, "edit");
return result;
}
// Save -------------------------------------------------------------------
#RequestMapping(value = "/brotherhood/edit", method = RequestMethod.POST, params = "save")
public ModelAndView save(#ModelAttribute("parade") final ParadeForm paradeForm, final BindingResult binding) {
ModelAndView result;
Parade parade;
Parade oldParade;
parade = this.paradeService.reconstruct(paradeForm, binding);
oldParade = this.paradeService.findOne(paradeForm.getId());
try {
for(AcmeFloat f : parade.getAcmeFloats()){
Collection<Parade> parades = f.getParades();
parades.add(parade);
f.setParades(parades);
this.acmeFloatService.save(f);
}
if(parade.getId() != 0){
Collection<AcmeFloat> paradesRemoved = oldParade.getAcmeFloats();
paradesRemoved.removeAll(parade.getAcmeFloats());
for(AcmeFloat f : paradesRemoved){
final Collection<Parade> parades = f.getParades();
parades.remove(parade);
f.setParades(parades);
this.acmeFloatService.save(f);
}
}
this.paradeService.save(parade);
result = new ModelAndView("redirect:list.do");
} catch (final ValidationException oops) {
result = this.createEditModelAndView(parade, "edit");
} catch (final Throwable oops) {
result = this.createEditModelAndView(parade, "parade.commit.error", "edit");
}
return result;
}
/*
final Parade paradeUpdated = this.paradeService.reconstruct(paradeForm, binding);
Collection<AcmeFloat> paradesRemoved = new ArrayList<>();
if (paradeForm.getId() != 0)
paradesRemoved = parade.getAcmeFloats();
if (paradeUpdated.getId() != 0)
paradesRemoved.removeAll(paradeUpdated.getAcmeFloats());
final Parade paradeSaved = this.paradeService.save(paradeUpdated);
for (final AcmeFloat f : paradeUpdated.getAcmeFloats()) {
final Collection<Parade> parades = f.getParades();
parades.add(paradeSaved);
f.setParades(parades);
this.acmeFloatService.save(f);
}
if (paradeUpdated.getId() != 0)
for (final AcmeFloat f : paradesRemoved) {
final Collection<Parade> parades = f.getParades();
parades.remove(parade);
f.setParades(parades);
this.acmeFloatService.save(f);
*/
// Delete -----------------------------------------------------------------
#RequestMapping(value = "/brotherhood/edit", method = RequestMethod.POST, params = "delete")
public ModelAndView delete(final Parade parade, final BindingResult binding) {
ModelAndView result;
try {
this.paradeService.delete(parade);
result = new ModelAndView("redirect:list.do");
} catch (final Throwable oops) {
result = this.createEditModelAndView(parade, "parade.commit.error", "edit");
}
return result;
}
// Save in Final Mode -----------------------------------------------------
#RequestMapping(value = "/brotherhood/edit", method = RequestMethod.POST, params = "finalMode")
public ModelAndView finalMode(#Valid final Parade parade, final BindingResult binding) {
ModelAndView result;
if (binding.hasErrors())
result = this.createEditModelAndView(parade, "edit");
else
try {
parade.setIsDraft(false);
this.paradeService.save(parade);
result = new ModelAndView("redirect:list.do");
} catch (final Throwable oops) {
result = this.createEditModelAndView(parade, "parade.commit.error", "edit");
}
return result;
}
// Show -------------------------------------------------------------------
#RequestMapping(value = "/public/show", method = RequestMethod.GET)
public ModelAndView show(#RequestParam final int paradeId) {
ModelAndView result;
Parade parade;
parade = this.paradeService.findOne(paradeId);
Assert.notNull(parade);
Assert.isTrue(parade.getIsDraft());
result = new ModelAndView("parade/public/" + "show");
result.addObject("parade", parade);
// result.addObject("messageCode", null);
return result;
}
// Ancillary Methods ------------------------------------------------------
protected ModelAndView createEditModelAndView(final Parade parade, final String method) {
ModelAndView result;
result = this.createEditModelAndView(parade, null, method);
return result;
}
protected ModelAndView createEditModelAndView(final Parade parade, final String messageCode, final String method) {
final ModelAndView result;
final Brotherhood brotherhood;
final Collection<AcmeFloat> acmeFloats;
final UserAccount userAccount = LoginService.getPrincipal();
brotherhood = this.brotherhoodService.findPrincipal();
acmeFloats = this.acmeFloatService.findAcmeFloats(userAccount.getId());
result = new ModelAndView("parade/brotherhood/" + method);
result.addObject("brotherhood", brotherhood);
result.addObject("acmeFloats", acmeFloats);
result.addObject("parade", parade);
result.addObject("messageCode", messageCode);
return result;
}
}
In the Create case, the flow is the next one: When in the create view after filling form and sending, save() method is called in ParadeController. Its input is a ParadeForm object with id=0. "parade" (the new Parade in this case and the updated Parade in case of edition) and "oldParade" (null in this case but the Parade before the update in update case) objects are created and declared anyway. Then, we go into the try/catch. First, it get the parade's acmeFloats in order to update them adding in their parades collection the just created Parade. But, at first attempt of saving, it throws the following thing:
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance
Saving first the new Parade gives the same results. I was intended to, in case it is an edition (so the Parade existed before and can have had AcmeFloats), find the AcmeFloats that have been removed from the Parade and update them and then save the Parade. So I needed the oldParade in edit case to check which AcmeFloats I have to remove the Parade from.
Also, I don't know whether I have to do all this thing with the cascade in #ManyToMany, but just saving the Parade once reconstructed, but it doesn't work anyway, so I decided to post that part of the code so that you figure out how would it work without the cascade.
I've been having troubles with this issue for the last month and before. Thanks in advance.
EDIT 1:
When I put a flush() after saving in the repository, it throws the following exception at saving:
org.springframework.orm.jpa.JpaSystemException: Exception occurred inside getter of domain.Parade.acmeFloats; nested exception is org.hibernate.PropertyAccessException: Exception occurred inside getter of domain.Parade.acmeFloats

JAXB This will cause infinitely deep XML

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();
}
}
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

Categories