I work with Spring JPA and have the following entity:
#Entity
#Table(name = Constants.ENTITY_TABLE_PREFIX + "ENTRY")
#XmlAccessorType(XmlAccessType.NONE)
#XmlRootElement(name = "monObj_info")
public class EntryXML implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
#XmlAttribute
private long id;
#Column(name = "ip_address", nullable = true)
#XmlElement
private String ip_address;
#Column(name = "network_element_name", nullable = false)
#XmlElement
private String network_element_name;
public EntryXML() {}
public EntryXML(long id, String ip_address, String network_element_name) {
super();
this.id = id;
this.ip_address = ip_address;
this.network_element_name = network_element_name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getIp_address() {
return ip_address;
}
public void setIp_address(String ip_address) {
this.ip_address = ip_address;
}
public String getNetwork_element_name() {
return network_element_name;
}
public void setNetwork_element_name(String network_element_name) {
this.network_element_name = network_element_name;
}
}
and the endpoint:
#RestController
public class EntryXMLEndpoint {
#Autowired
private IEntryXMLService service;
#RequestMapping(value = "/restxml", produces = { "application/xml" })
public EntryXML findEntries() {
EntryXML record = service.findById(1);
return record;
}
}
Now the requested response is:
<monObj_info id="1">
<atribute name="ip_address" value="xx.xxx.xxx.x"/>
<atribute name="network_element_name" value="xxxxxx"/>
</monObj_info>
Of course what I get is :
<monObj_info id="1">
<ip_address>xx.xxx.xxx.x</ip_address>
<network_element_name>xxxxxx</network_element_name>
</monObj_info>
I read similar posts , but the problem is I cannot create a List with the required elements inside my Entity Class, since it will not map with any column in the respective table, any suggestions?
You can achieve your goal in a straight-forward but somewhat hackish way.
Since you don't want the ip_address and network_element_name properties
to be marshalled and unmarshalled directly, you need to remove their #XmlElement annotation
and add #XmlTransient.
Instead, you want some <atribute name="..." value="..." /> elements marshalled and unmarshalled.
Therefore you need to add the following things to your EntryXML class:
an attributes property holding a list of attributes.
It is annotated with #XmlElement so that it will be part of XML marshalling and unmarshalling.
It is annotated with #Transient so that it will not be part of database persistence.
a simple helper class Attribute for holding name and value.
name and value are annotated with #XmlAttribute so that they will be part of XML marshalling and unmarshalling.
a Marshal Event Callback (beforeMarshal)
for doing the conversion from ip_address and network_element_name
to the attributes list.
an Unmarshal Event Callback (afterUnmarshal)
for doing the opposite conversion.
#XmlElement(name = "atribute")
#Transient // from package javax.persistence
private List<Attribute> attributes;
// there is no need for getAttributes and setAttributes methods
private static class Attribute {
#SuppressWarnings("unused") // called by the unmarshaller
Attribute() {
}
Attribute(String name, String value) {
this.name = name;
this.value = value;
}
#XmlAttribute
private String name;
#XmlAttribute
private String value;
}
#SuppressWarnings("unused") // this method is called only by the marshaller
private boolean beforeMarshal(Marshaller marshaller) {
attributes = new ArrayList<>();
attributes.add(new Attribute("ip_address", ip_address));
attributes.add(new Attribute("network_element_name", network_element_name));
return true;
}
#SuppressWarnings("unused") // this method is called only by the unmarshaller
private void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
if (attributes != null) {
for (Attribute attribute : attributes) {
switch (attribute.name) {
case "ip_address":
ip_address = attribute.value;
break;
case "network_element_name":
network_element_name = attribute.value;
break;
}
}
}
}
Then the XML output will look like this:
<monObj_info id="1">
<atribute name="ip_address" value="xx.xxx.xxx.x"/>
<atribute name="network_element_name" value="xxxxxx"/>
</monObj_info>
Related
I have a spring rest api application that is using HATEOAS/PagingAndSortingRepository to do most of the heavy lifting.
I have implemented caching using guava but I am having issues where when the user cancels the request midway through an api call, it caches the incomplete json and re-serves it for 60 seconds.
I am trying to use the unless="" parameter of the #Cacheable annotation. Previously, I just used unless="#result == null" but that does not handle incomplete or invalid json.
This does not seem to work either. So now I am trying to use com.google.gson.JsonParser to parse the result and invalidate if applicable.
Repository
#RepositoryRestResource(path = "products", collectionResourceRel = "products")
public interface ProductEntityRepository extends PagingAndSortingRepository<ProductEntity, String> {
JsonParser parser = new JsonParser();
#Cacheable(value = CacheConfig.STORE_CACHE)
ProductEntity findByName(String name);
}
Cache Config
public final static String PRODUCTS_CACHE = "products";
#Bean
public Cache productsCache() {
return new GuavaCache(PRODUCTS_CACHE, CacheBuilder.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.build());
}
How do I detect invalid json in the unless="" parameter?
I figured out my own issue!
When I interrupted the api request to localhost/products and re-requested, I finally saw an error about not being able to fetch a onetomany mapping. I believe the error was lazy initialization error for a collection.
I solved this issue by adding #LazyCollection(LazyCollectionOption.FALSE) to my models where the #OneToMany and #ManyToOne mappings were decalared.
For example:
#Entity(name = "product")
#Table(name = "products", schema = "${DB_NAME}", catalog = "")
public class ProductEntity {
private Integer id;
private String name;
private List shipments = new ArrayList<>();
#Id
#Column(name = "id", nullable = false)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name", nullable = false, length = 10)
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "shipmentID", targetEntity=ShipmentEntity.class)
#LazyCollection(LazyCollectionOption.FALSE)
public Collection<ShipmentEntity> getShipments() { return shipments; }
public void setShipments(Collection<ShipmentEntity> shipments) { this.shipments = shipments; }
}
I need some help to map Entity to DTO using Model Mapper.
Here are my two pojos
#Data
public class ClientDTO {
private UUID id;
#NotNull
private String name;
private String description;
private String contactEmail;
}
#Data
#Entity
public class Client {
#Id
private String id;
#NotNull
private String name;
private String description;
#NotNull
private String contactEmail;
}
When am trying to convert between Client to ClientDTO id is rendered as null. I tried writing a PropertyMap and a converter but none of them is working for me.
I went through the documentation and was able to find a solution to the problem. Here is the soln.
Initialization
private PropertyMap<Client, ClientDTO> clientMap;
private ModelMapper clientToClientDtoMapper;
Defining PropertyMap and Converter
clientToClientDtoMapper = new ModelMapper();
Converter<Client, UUID> uuidConverter = new AbstractConverter<Client, UUID>() {
protected UUID convert(Client source) {
return UUID.fromString(source.getId());
}
};
clientMap = new PropertyMap<Client, ClientDTO>() {
protected void configure() {
try {
using(uuidConverter).map(source).setId(null);
} catch (Exception ex) {
System.out.println("Error.");
}
}
};
clientToClientDtoMapper.addMappings(clientMap);
Helper method to convert from Entity to DTO
private ClientDTO convertToDto(Client client) {
ClientDTO clientDTO = clientToClientDtoMapper.map(client, ClientDTO.class);
return clientDTO;
}
I've worked with Spring and Hibernate. Now having a look at Spring Data JPA (2.0.3) with JPA 2.2
AgencyTicketType
#Entity
#Table(name = "agency_ticket_type", catalog = "test")
public class AgencyTicketType implements java.io.Serializable {
private Long id;
private String name;
private Agency agency;
private Set<AgencyTicketCategory> agencyTicketCategories = new HashSet<AgencyTicketCategory>(0);
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#Column(name = "name", nullable = false, length = 100)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "agency_id", nullable = false)
public Agency getAgency() {
return this.agency;
}
public void setAgency(Agency agency) {
this.agency = agency;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "agencyTicketType")
public Set<AgencyTicketCategory> getAgencyTicketCategories() {
return this.agencyTicketCategories;
}
public void setAgencyTicketCategories(Set<AgencyTicketCategory> agencyTicketCategories) {
this.agencyTicketCategories = agencyTicketCategories;
}
}
AgencyTicketCategory
#Entity
#Table(name = "agency_ticket_category", catalog = "waytest")
public class AgencyTicketCategory implements java.io.Serializable {
private Long id;
private AgencyTicketType agencyTicketType;
private String name;
private BigDecimal price;
private Set<TripTicket> tripTickets = new HashSet<TripTicket>(0);
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "id", unique = true, nullable = false)
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "agency_ticket_type_id", nullable = false)
public AgencyTicketType getAgencyTicketType() {
return this.agencyTicketType;
}
public void setAgencyTicketType(AgencyTicketType agencyTicketType) {
this.agencyTicketType = agencyTicketType;
}
#Column(name = "name", nullable = false, length = 100)
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
#Column(name = "price", nullable = false, precision = 8)
public BigDecimal getPrice() {
return this.price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
#OneToMany(fetch = FetchType.LAZY, mappedBy = "agencyTicketCategory")
public Set<TripTicket> getTripTickets() {
return this.tripTickets;
}
public void setTripTickets(Set<TripTicket> tripTickets) {
this.tripTickets = tripTickets;
}
}
Repository
public interface TicketTypeRepository extends JpaRepository<AgencyTicketType, Long> {
#EntityGraph(attributePaths={ "agencyTicketCategories" }, type=EntityGraphType.LOAD)
#Query("select type from AgencyTicketType type where type.agency.code=?1")
List<AgencyTicketType> findByAgency(String agencyCode);
}
Service
#Service
public class TicketServiceImpl implements TicketService {
#Autowired private TicketTypeRepository ticketType;
#Transactional(readOnly=true)
#Override
public List<AgencyTicketType> findByName(String code) {
return ticketType.findByAgency(code);
}
}
When debugged on Service, it seems, the query eagerly fetches all the lazy loaded properties - agency, agencyTicketCategories - and all their inner lazy loaded properties, which leads to JSON serilization error.
Need to fetch only these
AgencyTicketTypes [
{
id, name,
agencyTicketCategories [
{id,name,price},....
]
},.....
]
Can I do this with #EntityGraph? What I am missing?
Specifying lazy loading is only a hint for the JPA provider. Depending on the provider you use (Hibernate, EclipseLink etc.) it may be completely ignored and the dependencies may be eagerly fetched.
What you need to do is configure how your classes are mapped to json. Assuming you are using Jackson you may need to use annotations like #JsonIgnore or #JsonView. You may also map your class that only has the fields you need.
You can use Jackson annotations #JsonBackReference/#JsonManagedReference. They address problem of infinite recursion with bidirectional links in object model. As far as I understand it is your case.
See http://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion for more information.
One thing to point is that debugging while the transaction is open (touching the collection) will cause it to be loaded even if at real time it doesn't .. the other thing is that as #Apokralipsa mentioned , LAZY loading is just a hint that can be totally ignored and should never be relied upon whatever technique you are using
I have a model that looks something like this:
#Entity
public class MyModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(unique = true, nullable = false)
#RestResource(exported = false)
private int pk;
#Column(unique = true, nullable = false)
private String uuid = UUID.randomUUID().toString();
#Column(nullable = false)
private String title;
public int getPk() {
return pk;
}
public void setPk(int pk) {
this.pk = pk;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
As you can see I have an auto-incrementing PK as my ID for the model, but also a random UUID. I want to use the PK in the database as the primary key, but want to use the UUID as a public facing ID. (To be used in URLs etc.)
My repository looks like this:
#RepositoryRestResource(collectionResourceRel = "my-model", path = "my-model")
public interface MyModelRepository extends CrudRepository<MyModel, String> {
#RestResource(exported = false)
MyModel findByUuid(#Param("uuid") String id);
}
As you can see I've set the repository to use a String as the ID.
Finally I set the entity lookup in a config file like this:
#Component
public class RepositoryEntityLookupConfig extends RepositoryRestConfigurerAdapter {
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.withEntityLookup().forRepository(MyModelRepository.class, MyModel::getUuid, MyModelRepository::findByUuid);
}
}
This works perfectly well for GET and POST requests, but for some reason I get an error returned on PUT and DELETE methods.
o.s.d.r.w.RepositoryRestExceptionHandler : Provided id of the wrong type for class MyModel. Expected: class java.lang.Integer, got class java.lang.String
Anyone know what might be causing this? I don't understand why it's expecting an Integer.
I may be doing something stupid as I'm quite new to the framework.
Thanks for any help.
The identifier of your domain object is obviously of type int. That means, your repository needs to be declared as extends CrudRepository<MyModel, Integer>.
I'm populating an <s:select> from database. The action class is model-driven.
#Namespace("/admin_side")
#ResultPath("/WEB-INF/content")
#ParentPackage(value="struts-default")
public final class TestAction extends ActionSupport implements Serializable, ValidationAware, Preparable, ModelDriven<Transporter>
{
#Autowired
private final transient SharableService sharableService=null;
private static final long serialVersionUID = 1L;
private Transporter transporter; //Getter and setter
private Long transporterId; //Getter and setter.
private List<Transporter> transporters; //Getter only.
#Action(value = "Test",
results = {
#Result(name=ActionSupport.SUCCESS, location="Test.jsp"),
#Result(name = ActionSupport.INPUT, location = "Test.jsp")},
interceptorRefs={#InterceptorRef(value="defaultStack", params={"validation.validateAnnotatedMethodOnly", "true", "validation.excludeMethods", "load"})})
public String load() throws Exception
{
return ActionSupport.SUCCESS;
}
#Validations(
requiredFields={#RequiredFieldValidator(fieldName="transporterId", type= ValidatorType.FIELD, key = "transporter.required")})
#Action(value = "testInsert",
results = {
#Result(name=ActionSupport.SUCCESS, location="Test.jsp", params={"namespace", "/admin_side", "actionName", "Test"}),
#Result(name = ActionSupport.INPUT, location = "Test.jsp")},
interceptorRefs={#InterceptorRef(value="defaultStack", params={"validation.validateAnnotatedMethodOnly", "true"})})
public String insert() {
System.out.println("Selected item in the drop box : "+transporterId);
return ActionSupport.SUCCESS;
}
#Override
public void prepare() throws Exception {
transporters=sharableService.getTransporterList();
}
#Override
public Transporter getModel() {
return transporter;
}
}
and the following is <s:select> :
<s:select id="transporterId"
name="transporterId"
list="transporters"
value="transporterId"
listKey="transporterId"
listValue="transporterName"
headerKey="" headerValue="Select"
listTitle="transporterName"/>
This works perfectly.
I need this <s:select> in another action class which implements ModelDriven<ZoneTable>.
The table structure is simple, transporter->zone_table->country->state->city. There exists a one-to-many relationship between these tables.
How can we have a model driven action class implementing ModelDrven<ZoneTable> in which Transporter can be mapped to <s:select>, something like?
#Namespace("/admin_side")
#ResultPath("/WEB-INF/content")
#ParentPackage(value="struts-default")
public final class ZoneAction extends ActionSupport implements Serializable, ValidationAware, Preparable, ModelDriven<ZoneTable>
{
#Autowired
private final transient ZoneService zoneService=null;
#Autowired
private final transient SharableService sharableService=null;
private ZoneTable entity=new ZoneTable(); //Getter and setter.
private Long transporterId; //Getter and setter.
private List<Transporter> transporters; //Getter only.
#Override
public ZoneTable getModel() {
return entity;
}
#Override
public void prepare() throws Exception {
transporters=sharableService.getTransporterList();
}
}
Doing like this doesn't work. It doesn't set the value of transporterId upon submission, since the action class is implementing ModelDriven<ZoneTable> and not ModelDriven<Transporter> like the first case.
Is this possible using the model driven approach?
EDIT:
ZoneTable.java
public class ZoneTable implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "zone_id", nullable = false)
private Long zoneId;
#Column(name = "zone_name", length = 45)
private String zoneName;
#JoinColumn(name = "transporter_id", referencedColumnName = "transporter_id")
#ManyToOne(fetch = FetchType.LAZY)
private Transporter transporterId;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "zoneTable", fetch = FetchType.LAZY)
private Set<ZoneCharge> zoneChargeSet;
#OneToMany(mappedBy = "zoneId", fetch = FetchType.LAZY)
private Set<Country> countrySet;
//Getters and setters + constructors.
}
Zone.jsp
<s:form namespace="/admin_side" action="Zone" validate="true" id="dataForm" name="dataForm" cssClass="search_form general_form">
<s:label key="label.zone.name" for="zone"/>
<s:textfield id="zoneName" name="zoneName" cssClass="validate[required, maxSize[45], minSize[2]] text-input text"/>
<s:fielderror fieldName="zoneName"/>
<s:label key="label.transporter.name" for="transporterId"/>
<s:select id="transporterId" name="transporterId" list="transporters" value="transporterId" listKey="transporterId" listValue="transporterName" headerKey="" headerValue="Select" listTitle="transporterName"/>
<s:fielderror fieldName="transporterId"/>
<s:text name="label.submit"/>
<s:submit id="btnSubmit" name="btnSubmit" value="Submit" action="AddZone"/>
</s:form>
Since this post has already a lot of code, I'm not posting the action class ZoneAction.java here. In case, it is needed, it is available here.
You need a converter to convert transporterId to Transporter Object. It goes like this:
package com.converter;
public class TransporterConverter extends StrutsTypeConverter {
#Override
public Object convertFromString(Map map, String[] strings, Class type) {
String value = strings[0]; // The value of transporterId submitted from the jsp
if (value != null && value.length() > 0) {
try {
Long longVal = Long.valueOf(value);
//Integer intVal = Integer.valueOf(value);
if (type == Transporter.class) {
Transporter data = find_transporter_from_the_back_by_transporter_id_using_longVal;
return data;
}
} catch (Exception ex) {}
}
return null;
}
#Override
public String convertToString(Map map, Object o) {
if ((o instanceof Transporter)) {
Transporter data = (Transporter) o;
//return the id of the Transporter Object
}
return null;
}
}
The next thing to do is to map this class in a file called xwork-conversion.properties. This file must reside in your classpath i.e. in classes directory. Enter the following entries in xwork-conversion.properties
package_of_transporter_class.Transporter=com.converter.TransporterConverter
I have not tested it, but I think it should work.
If you need more information on how type converters work, follow this url.