Wanted to know how is #Version annotation in Spring Data REST put to use for ETags, I do not see the ETags populated for some reason
#Entity
#EntityListeners(AuditingEntityListener.class)
public class Venue implements Serializable {
private static final long serialVersionUID = -5516160437873476233L;
private Long id;
...
// other properties
private Long version;
private Date lastModifiedDate;
// getters & setters
#JsonIgnore
#LastModifiedDate
public Date getLastModifiedDate() {
return lastModifiedDate;
}
#Version
#Column
public Long getVersion() {
return version;
}
Going by the docs this should give me an Etag Value? as seen in the snippet from the library
protected HttpHeaders prepareHeaders(PersistentEntity<?, ?> entity, Object value) {
// Add ETag
HttpHeaders headers = ETag.from(entity, value).addTo(new HttpHeaders());
// Add Last-Modified
AuditableBeanWrapper wrapper = getAuditableBeanWrapper(value);
however, given the entity & following configuration, I still get a null for Version.
My Application has the following
#SpringBootApplication
#EnableEntityLinks
#EnableJpaAuditing
public class GabbarSinghApplication
And the Rest Repository is as follows
#RepositoryRestResource(collectionResourceRel = "venue", path = "venues")
public interface VenueRepository extends JpaRepository<Venue, Long> {
While I haven't got to test these methods yet with the headers etc, a simple POST request on http://localhost:8080/workshops gives a 500 because of null pointer exception at getting ETag header value from value of version property.
Update
Moved to #javax.persistence.Version for the entities, I still do not get an ETag header in the response headers.
Here's a failing unit test
#Before
public void setUp() throws Exception {
XStream xstream = new XStream();
ObjectInputStream in = xstream.createObjectInputStream(venuesXml.getInputStream());
leela = (Venue) in.readObject();
paul = (Venue) in.readObject();
taj = (Venue) in.readObject();
LOGGER.debug("Initialised Venues from xml file {}", venuesXml.getFilename());
}
#Test
public void testEtagHeaderIsAutoGeneratedOnResourceCreation() {
final HttpEntity<Venue> httpEntity = new HttpEntity<Venue>(taj, headers);
ResponseEntity<ResourceSupport> response = restTemplate.exchange(BASE_LOCATION
+ VENUES_ENDPOINT, HttpMethod.POST, httpEntity,
new ParameterizedTypeReference<ResourceSupport>() {
});
assertTrue("Response should contain ETag header", null != response.getHeaders().getETag());
This assertion fails.
With Spring Data JPA, you need to use #javax.persistence.Version. #org.springframework.data.annotation.Version is the annotation to use for other Spring Data modules.
I faced the same problem and after hours and hours I realized the following things that led me to the enlightenment =P
Spring Data Rest provides ETag support for optimistic concurrency control ONLY after version 2.3.0. See this bug published about a year ago. Previous versions of Spring Data Rest will NOT populate the ETag header.
For Spring Boot applications (our case), you need to use Spring Boot 1.3.0.BUILD-SNAPSHOT or higher in order to be able to setup a spring-boot-starter-data-rest that depends on Spring Data Rest 2.4.1 (higher than 2.3.1 which is exactly what we need :P).
In my case I was using Spring Boot 1.2.7 and whenever I installed the spring-boot-starter-data-rest dependency I ended up getting the Spring Data Rest 2.2.0 which don't has the ETag support. After upgrading Spring Boot in my project and reinstalled the dependencies my REST API started to retrieve the ETag header =D
Related
I am getting a "Failed to convert from type [java.lang.String] to type [#javax.persistence.Column java.util.Date] for value '1999-12-20'" when I use json-patch, but I do not get this error with PUT, POST or event PATCH requests with json-merge-patch;
I am using Spring Boot version 2.1.8, with Spring Data Rest.
I have an entity with a field similar to the following (only the name of the variable and column are different):
#Column(name = "mydate")
private Date mydate;
The field is updated as expected when I issue a JSON merge patch request with the body:
{"mydate": "1999-12-20"}
It is also updated correctly with POST and PUT requests.
However, if I issue a json-patch with the following command (and Content-Type application/json-patch+json):
[{"op":"replace","path":"mydate","value":"2018-08-09"}]
I get the error mentioned above.
I don't actually need a workaround for this, as I am using the JSON Merge Patch for that.
I'd like to understand how to make the conversion that works for the other requests work for json-patch as well.
Here is a suggestion: Since Java 8 class Date should not be used, and it should be replaced with some implementation of Temporal interface. For example in your case LocalDate. And as far as Annotations for such property you'd want to annotate it as follows:
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
public LocalDate myDate;
For details refer to the answer to this question: Spring Data JPA - ZonedDateTime format for json serialization
This is a workaround, not a proper solution.
I still don't understand why this happens, but here are two workarounds that can be helpful for the next poor soul who stumbles in this issue:
Use JSON Merge Patch
Patch requests with JSON Merge Patch have their date fields properly interpreted by SDR. If possible, simply switching from using JSON Patch to JSON Merge Patch will solve the situation.
Use a dummy field
I couldn't find a way to help Spring Data Rest to use a Converter when it receives a JSON Patch request, so creating a dummy String field in your DTO (or worst ― in your Entity) will fix the issue.
If you can't or don't want to avoid using json-patch, this may be helpful.
This is not a beautiful solution and one might call you names upon seeing it, but it will work.
Consider the following ― worst case scenario ― code :
#Entity
public class MyRenderVouz {
...
#Column(name="important_date")
private LocalDate importantDate;
#Transient
private String strChangeImportantDate;
public void setStrChangeImportantDate(String newDate) {
setImportantDate(LocaleDate.parse(newDate);
}
public String getStrChangeImportantDate() {
return this.importantDate;
}
...
}
In your JSON-PATCH, simple use the strChangeImportantDate instead of importantDate, for example, consider you want to update the importantDate of the second instance of MyRendezVouz that belong to a calendar:
Calendar
L importantRenderVouz []
L myRenderVouz
L myRenderVouz
L importantDate // target date
L myRenderVouz
Then you can use:
[
{"op":"replace","path":"importantRenderVouz/1/strImportantDate","value":"2020-01-01"},
]
What I do when I have to perform patch request for an Entity with LocalDateTime attribute:
Lets for clarity say that your Entity having attribute LocalDateTime date is modeled with an Event.class:
#NoArgsConstructor
#Getter
#Setter
#Entity(name = "events")
...
public class Event {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long eventId;
...
#Column(nullable = false)
private LocalDateTime date;
...
1) Se externally the LocalDateTime format pattern in application.properties file:
config.datetime.format.pattern=yyyy-MM-dd HH:mm
2) Create a class AppConfig.class (or something like that) to extract the value from the application.properties file:
#Configuration
public class AppConfig {
#Value("${config.datetime.format.pattern}")
private String formatPattern;
public DateTimeFormatter getDateTimeFormatter(){
return DateTimeFormatter.ofPattern(formatPattern);
}
}
3) Add the AppConfig dependency to your service class:
#Service
public class EventService {
private final EventRepository eventRepo;
private final EventDtoMapper eventDtoMapper;
private final AppConfig appConfig;
...
#Autowired
public EventService(EventRepository eventRepo, EventDtoMapper eventDtoMapper, AppConfig appConfig, ...) {
this.eventRepo = eventRepo;
this.eventDtoMapper = eventDtoMapper;
this.appConfig = appConfig;
...
}
4) Write the service code for the function that will do the partial update:
public EventDtoForCreate updatePartially(Long id, Map<String, String> fields) {
Optional<Event> found = eventRepo.findById(id);
if (found.isEmpty()) {
// custom exception
throw new ResourceNotFoundException("Failed to find event");
}
fields.forEach((key, value) -> {
Field field = ReflectionUtils.findField(Event.class, key);
if (field != null) {
field.setAccessible(true);
if (field.getAnnotatedType().getType().equals(LocalDateTime.class)) {
ReflectionUtils.setField(field, found.get(), LocalDateTime.parse(value, appConfig.getDateTimeFormatter()));
} else {
ReflectionUtils.setField(field, found.get(), value);
}
}
});
return eventDtoMapper.toDtoCreate(eventRepo.save(found.get()));
}
5) Finally, create the controller for the patch request:
#PatchMapping(
value = "/{id}",
produces = {MediaType.APPLICATION_JSON_VALUE}
)
public ResponseEntity<EventDtoCreate> updatePartially(
#PathVariable(name = "id") Long userId,
#RequestBody Map<String, String> fields
) {
return ResponseEntity.ok(eventService.updatePartially(userId, fields));
}
I've created spring application for CRUD. I can easily write into server data like string,Long,blob but When I try to retrieve it from server. I've encountered with difficulty which byte array from server gives in BigInteger from server. How I could get data in byte array instead of BigInteger?When I write in insert byte array this data which column is BLOB. Here is my code
Repository
public interface ArriveRepository extends JpaRepository<ArriveEntity,Long>
{
#Query(value = "select arrive.time,air_lines.image,arrive.flight,arrive.destination_uzb," +
"arrive.destination_eng,arrive.destination_rus,arrive.status,arrive.status_time " +
"from arrive inner join air_lines on air_lines.id = arrive.airline_id where arrive.arrive_date = (:date1)",nativeQuery = true)
List<Object[]> getForArriveTerminal(#Param("date1") LocalDate date1);
}
When I retrieve data from server I'm using this class
ArriveTerminalDto
public class ArriveTerminalDto {
private String time;
private BigInteger logo;
private String flight;
private String destinationUzb;
private String destinationEng;
private String destinationRus;
private String status;
private String statusTime;
//getter setter}
Service class
public List<ArriveTerminalDto> getToShow(LocalDate date1)
{
List<ArriveTerminalDto> list = new ArrayList<>();
List<Object[]> list1 = arriveRepository.getForArriveTerminal(date1);
for(Object[] objects: list1)
{
ArriveTerminalDto arriveTerminalDto = new ArriveTerminalDto();
arriveTerminalDto.setTime((String)objects[0]);
arriveTerminalDto.setLogo((BigInteger) objects[1]);
arriveTerminalDto.setFlight((String) objects[2]);
arriveTerminalDto.setDestinationUzb((String) objects[3]);
arriveTerminalDto.setDestinationRus((String) objects[4]);
arriveTerminalDto.setDestinationEng((String) objects[5]);
arriveTerminalDto.setStatus((String) objects[6]);
list.add(arriveTerminalDto);
}
return list;
}
This code works but it didn't give me byte array from server.
When I try to change BigInteger into byt[] array it gives me following errors
from postman
{
"timestamp": "2019-01-28T09:33:52.038+0000",
"status": 500,
"error": "Internal Server Error",
"message": "java.math.BigInteger cannot be cast to [B",
"path": "/arrive/terminal/date=2019-01-27"
}
Changed Object into ArriveTerminalDto but still it give error my following repo
public interface ArriveRepository extends JpaRepository<ArriveEntity,Long>
{
#Query(value = "select arrive.time,air_lines.image,arrive.flight,arrive.destination_uzb," +
"arrive.destination_eng,arrive.destination_rus,arrive.status,arrive.status_time " +
"from arrive inner join air_lines on air_lines.id = arrive.airline_id where arrive.arrive_date = (:date1)",nativeQuery = true)
List<ArriveTerminalDto> getForArriveTerminal(#Param("date1") LocalDate date1);
}
Why don't you take a look at the Spring Content community project. This project allows you to associate content with Spring Data entities. Think Spring Data but for Content, or unstructured data. This can also give you REST endpoints for the content as well, like Spring Data REST.
This approach will give you a clear abstraction for your content with implementations for many different types of storage. It is stream-based, rather than byte-based. Using byte[] won't work if you want to transfer very large files. Also getting databases to stream properly is very idiosyncratic. You probably don't want to figure all that out yourself when Spring Content already has.
This is pretty easy to add to your existing projects. I am not sure if you are using Spring Boot, or not. I'll give a non-spring boot example:
pom.xml
<!-- Java API -->
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-jpa</artifactId>
<version>0.5.0</version>
</dependency>
<!-- REST API (if you want it) -->
<dependency>
<groupId>com.github.paulcwarren</groupId>
<artifactId>spring-content-rest</artifactId>
<version>0.5.0</version>
</dependency>
Configuration
#Configuration
#EnableJpaStores
#Import("org.springframework.content.rest.config.RestConfiguration.class")
public class ContentConfig {
// schema management
//
#Value("/org/springframework/content/jpa/schema-drop-mysql.sql")
private Resource dropContentTables;
#Value("/org/springframework/content/jpa/schema-mysql.sql")
private Resource createContentTables;
#Bean
DataSourceInitializer datasourceInitializer() {
ResourceDatabasePopulator databasePopulator =
new ResourceDatabasePopulator();
databasePopulator.addScript(dropContentTables);
databasePopulator.addScript(createContentTables);
databasePopulator.setIgnoreFailedDrops(true);
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource());
initializer.setDatabasePopulator(databasePopulator);
return initializer;
}
}
To associate content, add Spring Content annotations to your account entity.
ArriveEntity.java
#Entity
public class ArriveEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
.. existing fields...
#ContentId
private String contentId;
#ContentLength
private long contentLength = 0L;
// if you have rest endpoints
#MimeType
private String mimeType = "text/plain";
}
Create a "store":
ArrivEntityContentStore.java
#StoreRestResource(path="arriveEntityContent)
public interface ArrivEntityContentStore extends ContentStore<ArriveEntity, String> {
}
This is all you need to create REST endpoints # /arriveEntityContent. When your application starts, Spring Content will look at your dependencies (seeing Spring Content JPA/REST), look at your ArrivEntityContentStore interface and inject an implementation of that interface for JPA. It will also inject a #Controller that forwards http requests to that implementation. This saves you having to implement any of this yourself which I think is what you are after.
So...
To access content with a Java API, auto-wire ArrivEntityContentStore and use it methods.
Or to access content with a REST API:
curl -X POST /arriveEntityContent/{arriveEntityId}
with a multipart/form-data request will store the image in the database and associate it with the account entity whose id is itemId.
curl /arriveEntityContent/{arriveEntityId}
will fetch it again and so on...supports full CRUD.
There are a couple of getting started guides here. The reference guide is here. And there is a tutorial video here. The coding bit starts about 1/2 way through.
HTH
Try to change entity definition to handle byte[] directly, but suggest JPA to interpret it as Lob. You can do it with #Lob annotation:
public class ArriveTerminalDto {
private String time;
#Lob
private byte[] logo;
private String flight;
private String destinationUzb;
private String destinationEng;
private String destinationRus;
private String status;
private String statusTime;
}
Laster, as #Clijsters suggested, you can change your repo to return List<ArriveTerminalDto>.
I am using springBoot 2 and I am trying to validate the objects in a List via:
#RequestMapping(value = "/bets",
produces = {"application/json"},
consumes = {"application/json"},
method = RequestMethod.POST
)
void postBets(#RequestBody List<#Valid Bet> bets);
and Bet class has #NotNull annotations on certain attributes.
import javax.validation.constraints.NotNull;
public class Bet extends BetMessage {
#NotNull
private String categoryName;
#NotNull
private String marketName = null;
#NotNull
private OffsetDateTime startTime = null;
#NotNull
private String betName = null;
I have also added the spring-boot-starter-validation artifact to my build file but still no validation is happening.
As a workaround I have implemented the popular answer in the question below (ValidList class) and validation is working as expected; however I think that I am missing something obvious and the solution is now part of the validation library.
Validation of a list of objects in Spring
You may want to write a wrapper which contains your list of Bet because then your wrapper will conform to JavaBean specs and the validations can be applied.
Below Answer might help in this case.
#Valid on list of beans in REST service
We're using MongoDB and Spring Data in our Spring Boot app. Another developer has earlier written a service, using MongoTemplate's findAndModify method, and some additional code that uses that service. It worked fine, even in production.
I've branched and went on adding some new features (completely new code) and needed to call that service. Service doesn't work for me, even though I didn't even touch the service code. Moreover, even the code the other guy wrote before (it's a REST controller calling the service) doesn't work now. On the master branch, everything works like it should.
Here's the service:
#Autowired
private MongoTemplate mongo;
public void addRetention(Date when, String userId, Platform platform) {
LocalDateTime dateTime = LocalDateTime.ofEpochSecond(when.getTime()/1000, 0, ZoneOffset.UTC);
int yyyymm = 100 * dateTime.getYear() + dateTime.getMonthValue();
Retention r = mongo.findAndModify( // the exception is coming from this line
new Query(Criteria.where("userId").is(userId).and("yyyymm").is(yyyymm)),
new Update().inc("count", 1).set("platform", platform),
new FindAndModifyOptions().upsert(true).returnNew(true), Retention.class
);
addRealTimeRetention(userId, r, yyyymm);
}
How I call the service:
retentionService.addRetention(timeService.timeToUtil(usage2.getDate()),
usage2.getUserId(), platform);
And the stack trace on PasteBin. The exception message is:
IllegalArgumentException: Target bean is not of type of the persistent entity!
EDIT: Here's the Retention.java class:
#Document
#JsonInclude(JsonInclude.Include.NON_NULL)
#CompoundIndexes({
#CompoundIndex(name = "userid_yyyymm_idx", def = "{ userId: 1, yyyymm: 1 }")
})
public class Retention {
#Id
private String id;
#Indexed
private String userId;
#Indexed
private int yyyymm;
private int count;
private Platform platform;
// setters and getters...
}
FINAL EDIT: I solved this by deleting spring devtools dependency in pom.xml. I'm not sure how that dependency has anything to do with this exception. Found the solution here. Thank you to everyone who helped out.
I'm using Spring Boot 1.5.4, Spring Data REST, HATEOAS. I'm exposing REST endpoints to be consumed from a Angular client.
I'm using spring.data.rest.enable-enum-translation=true to convert enums. It works fine both in GET and POST requests exposed from Spring Data REST from repositories.
I added a custom method in a repository:
#Transactional(readOnly = true)
#PreAuthorize("isAuthenticated()")
public interface TransitCertificateRepository extends PagingAndSortingRepository<TransitCertificate, Long> {
#Query("SELECT t FROM TransitCertificate t WHERE :states IS NULL OR status IN (:states) ")
public Page<TransitCertificate> findAllByParameters(
#Param("states") #RequestParam(value = "states", required = false) List<TransitCertificateStatus> states, Pageable pageable);
This is the enum:
public enum TransitCertificateStatus {
PENDING, USED, CANCELED, ARCHIVED
}
This is the relevant part of the model:
#Entity
#EntityListeners(TransitCertificateListener.class)
public class TransitCertificate extends AbstractEntity {
private static final long serialVersionUID = 5978999252424024545L;
#NotNull(message = "The status cannot be empty")
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private TransitCertificateStatus status = TransitCertificateStatus.PENDING;
In rest-messages.properties I've translation for the enum like:
server.model.enums.TransitCertificateStatus.PENDING = Pending
server.model.enums.TransitCertificateStatus.USED = Used
When the client try to call my method findAllByParameters and sends a array of String (translated how the server sent back), the conversion on the server fails.
I don't understand why the conversion works in save() method, for example, but not in my method.
Furthemore if the client sends me 2 states, Spring returns this error:
Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value element [USED] did not match expected type [server.model.enums.TransitCertificateStatus (n/a)]
So I guess I've two problems:
For some reason Spring is not able to convert a String[] to a List<TransitCertificateStatus> even if the value is exactly the one defined in the TransitCertificateStatus (PENDING, USED, CANCELED, ARCHIVED)
Spring is not able to convert the String the client send, to the right enum when the value is one of that defined in rest-messages.properties (Pending, Used, etc).
Is there a way to solve the problem is a elegant way using internal facilities of Spring Data REST (I point out enum transation works in save() method) without reinventing the wheel?
I ended up to solve the problem in this way:
I created a custom #RepositoryRestController
I created my method
I get the enum from the client like a String and then I convert it. In this way the client can send also the translated string for the enum
This a piece of code:
#PostMapping(path = "/licensePlates/searches")
public ResponseEntity<?> search(#RequestBody(required = true) List<Filter> filters, Pageable pageable, Locale locale,
PersistentEntityResourceAssembler resourceAssembler) {
EngineType engineType = enumTranslator.fromText(EngineType.class, filterMap.get("engineType"));
You have to inject enumTranslation in this way:
#Autowired
private EnumTranslator enumTranslator;
Not sure is the best way but that solved my problem with little code.