After detecting a flaw in one of my web services I tracked down the error to the following one-liner:
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
This line was returning false when I positively knew that the list of domains contained a domain which name was equal to the provided name
. So after scratching my head for a while, I ended up splitting the whole line to see what was going on. I got the following in my debugging session:
Please notice the following line:
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
According to the debugger, domains
is a list with two elements. But after applying .stream().collect(Collectors.toList())
I get a completely empty list. Correct me if I'm wrong, but from what I understand, that should be the identity operation and return the same list (or a copy of it if we are strict). So what is going on here???
Before you ask: No, I haven't manipulated that screenshot at all.
To put this in context, this code is executed in a stateful request scoped EJB using JPA managed entities with field access in a extended persistence context. Here you have some parts of the code relevant to the problem at hand:
@Stateful
@RequestScoped
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class DomainResources {
@PersistenceContext(type = PersistenceContextType.EXTENDED) @RequestScoped
private EntityManager entityManager;
public boolean templateContainsDomainWithName(String name) { // Extra code included to diagnose the problem
MetadataTemplate template = this.getTemplate();
List<Domain> domains = template.getDomains();
List<Domain> domains2 = domains.stream().collect(Collectors.toList());
List<String> names = domains.stream().map(Domain::getName).collect(Collectors.toList());
boolean exists1 = names.contains(name);
boolean exists2 = this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
return this.getTemplate().getDomains().stream().anyMatch(domain -> domain.getName().equals(name));
}
@POST
@RolesAllowed({"root"})
public Response createDomain(@Valid @EmptyID DomainDTO domainDTO, @Context UriInfo uriInfo) {
if (this.getTemplate().getLastVersionState() != State.DRAFT) {
throw new UnmodifiableTemplateException();
} else if (templateContainsDomainWithName(domainDTO.name)) {
throw new DuplicatedKeyException("name", domainDTO.name);
} else {
Domain domain = this.getTemplate().createNewDomain(domainDTO.name);
this.entityManager.flush();
return Response.created(uriInfo.getAbsolutePathBuilder().path(domain.getId()).build()).entity(new DomainDTO(domain)).type(MediaType.APPLICATION_JSON).build();
}
}
}
@Entity
public class MetadataTemplate extends IdentifiedObject {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "metadataTemplate", orphanRemoval = true) @OrderBy(value = "creationDate")
private List<Version> versions = new LinkedList<>();
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) @OrderBy(value = "name")
private List<Domain> domains = new LinkedList<>();
public List<Version> getVersions() {
return Collections.unmodifiableList(versions);
}
public List<Domain> getDomains() {
return Collections.unmodifiableList(domains);
}
}
I've included both getVersions
and getDomains
methods because I have similar operations running flawlessly on versions. The only significant difference I'm able to find is that versions
are eagerly fetched while domains
are lazily fetched. But as far as I know the code is being executed inside a transaction and the list of domains is being loaded. If not I'd get a lazy initialization exception, wouldn't I?
UPDATE: Following @Ferrybig 's suggestion I've investigated the issue a bit further, and there doesn't seem to have anything to do with improper lazy loading. If I traverse the collection in a classic way I still can't get proper results using streams:
boolean found = false;
for (Domain domain: this.getTemplate().getDomains()) {
if (domain.getName().equals(name)) {
found = true;
}
}
List<Domain> domains = this.getTemplate().getDomains();
long estimatedSize = domains.spliterator().estimateSize(); // This returns 0!
domains.spliterator().forEachRemaining(domain -> {
// Execution flow never reaches this point!
});
So it seems that even when the collection has been loaded you still have that odd behavior. This seems to be a missing or empty spliterator implementation in the proxy used to manage lazy collections. What do you think?
BTW, this is deployed on Glassfish / EclipseLink
See Question&Answers more detail:
os