How to append specification based on condition in spring data jpa - spring-boot

I am creating a spring boot project and working on spring data jpa and currently I am using custom query to fetch data from db based on users selection and this is my page where user can select option based on their condition https://i.imgur.com/coO3BCJ.png
So, I googled it and found we can use specification but as I am very new to specification, so, I used specification and I want that based on users choice, it should keep adding specification, so, this is my conditional specification...
Specification<UserDetails> specification =
Specification.where(UserDetailsSpecification
.isAgeBetween(customSearch.getFromage(), customSearch.getToage()));
if(!customSearch.getManglik().isBlank()) {
specification.and(UserDetailsSpecification.isManglik
(customSearch.getManglik()));
}
if(!customSearch.getMaritalStatus().isBlank()) {
specification.and(UserDetailsSpecification
.hasMaritalStatus(customSearch.getMaritalStatus()));
}
if(!customSearch.getReligion().isBlank()) {
specification.and(UserDetailsSpecification.hasReligion
(customSearch.getReligion()));
}
if(!customSearch.getCaste().isBlank()) {
specification.and(UserDetailsSpecification.hasCaste
(customSearch.getCaste()));
}
if(!customSearch.getLocation().isBlank()) {
specification.and(UserDetailsSpecification.hasLocation
(customSearch.getLocation()));
}
listOfCustomSearch=userDetailsRepository
.findAll(specification, pageable);
List<UserDetails> listOfAllSearchedUsers = listOfCustomSearch.getContent();
but it is not appending the specification and just filtering the data based on only
Specification<UserDetails> specification = Specification.where(UserDetailsSpecification
.isAgeBetween(customSearch.getFromage(), customSearch.getToage()));
so, based on users selection, so, my final query should be something like this(If user has selected all fields):
Specification<UserDetails> specification = Specification.where(UserDetailsSpecification
.isAgeBetween(customSearch.getFromage(), customSearch.getToage())
.and(UserDetailsSpecification.isManglik(customSearch.getManglik()) .and(UserDetailsSpecification.hasMaritalStatus(customSearch.getMaritalStatus())) .and(UserDetailsSpecification.hasReligion(customSearch.getReligion()))
.and(UserDetailsSpecification.hasCaste(customSearch.getCaste()))
.and(UserDetailsSpecification.hasLocation(customSearch.getLocation()))))
But suppose if user has selected only let suppose 3 or 4 fields, so, my final specification should be something like below:(This specification should be completely depends upon user selection)
Specification<UserDetails> specification = Specification.where(UserDetailsSpecification
.isAgeBetween(customSearch.getFromage(), customSearch.getToage())
.and(UserDetailsSpecification.isManglik(customSearch.getManglik())
.and(UserDetailsSpecification.hasLocation(customSearch.getLocation()))))
Currently it is not appending specification based on users selection, so, please help me in adding specification based on users selections

Finally, I found the solution and we can create customized specification like this below:
public static Specification<UserDetails> getSpecs(String gender, int fromAge, int toAge, String manglikStatus, String maritalStatus, String religion, String caste, String location){
Specification<UserDetails> spec = null;
Specification<UserDetails> temp = null;
if(!gender.isBlank() && !gender.isEmpty() && gender!=null && !gender.contains("$")) {
spec = getSpecsForGenderDetails(gender);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(fromAge!=0 || toAge!=0) {
spec = isAgeBetween(fromAge, toAge);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(!manglikStatus.isBlank() && !manglikStatus.isEmpty() && manglikStatus!=null && !manglikStatus.contains("$")) {
spec = isManglik(manglikStatus);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(!maritalStatus.isBlank() && !maritalStatus.isEmpty() && maritalStatus!=null && !maritalStatus.contains("$")) {
spec = hasMaritalStatus(maritalStatus);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(!religion.isBlank() && !religion.isEmpty() && religion!=null && !religion.contains("$")) {
spec = hasReligion(religion);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(!caste.isBlank() && !caste.isEmpty() && caste!=null && !caste.equalsIgnoreCase("select") && !caste.contains("$")) {
spec = hasCaste(caste);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
if(!location.isBlank() && !location.isEmpty() && location!=null && !location.contains("$")) {
spec = hasLocation(location);
temp = spec!=null?Specification.where(spec).and(temp):temp;
}
return temp;
}
And based on that we can define our method like this:
private static Specification<UserDetails> getSpecsForGenderDetails(String gender) {
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("gender"),gender);
});
}
private static Specification<UserDetails> isAgeBetween(int fromAge, int toAge){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.between(root.get("age"), fromAge, toAge);
});
}
private static Specification<UserDetails> isManglik(String manglikStatus){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(criteriaBuilder.lower(root.get("manglikStatus")),"%" +manglikStatus.toLowerCase() +"%");
});
}
private static Specification<UserDetails> hasMaritalStatus(String maritalStatus){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(criteriaBuilder.lower(root.get("maritalStatus")),"%" +maritalStatus.toLowerCase() +"%");
});
}
private static Specification<UserDetails> hasReligion(String religion){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(criteriaBuilder.lower(root.get("religion")),"%" +religion.toLowerCase() +"%");
});
}
private static Specification<UserDetails> hasCaste(String caste){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(criteriaBuilder.lower(root.get("caste")),"%" +caste.toLowerCase() +"%");
});
}
private static Specification<UserDetails> hasLocation(String presentState){
return ((root, query, criteriaBuilder) -> {
return criteriaBuilder.like(criteriaBuilder.lower(root.get("presentState")),"%" +presentState.toLowerCase() +"%");
});
}

Related

Not able to filter elements using an enum

protected static double averagePrice( List<ComputerComponent> list ) {
return list.stream()
// .filter( line-> EnumUtils.isValidEnum(ComputerComponentCategory.class, line.getCategory()) )
// .filter( line-> isInEnum( line.getCategory(), ComputerComponentCategory.class) )
// .filter( line-> inEnum(line.getCategory(),EnumUtils.getEnumMap(ComputerComponentCategory.class ).keySet() ))
.filter( line ->
line.getCategory().contains("CPU")
|| line.getCategory().contains("GPU")
|| line.getCategory().contains("Monitor")
|| line.getCategory().contains("Keyboard")
|| line.getCategory().contains("Mouse")
|| line.getCategory().contains("Storage")
|| line.getCategory().contains("Memory"))
.mapToDouble(ComputerComponent::getPrice)
.average()
.orElseThrow(NoSuchElementException:: new);
}
I have an enum as
public enum ComputerComponentCategory {
CPU("CPU"),
MONITOR("Monitor"),
KEYBOARD("Keyboard"),
MOUSE("Mouse"),
GPU("GPU"),
MEMORY("Memory"),
STORAGE("Storage"),
NULL("NOT DEFINED");
private String label;
ComputerComponentCategory(String label) {
this.label = label;
}
public String getLabel() {
return this.label;
}
public static ComputerComponentCategory getValue(String label) {
switch(label) {
case "CPU":
return CPU;
case "Monitor":
return MONITOR;
case "Keyboard":
return KEYBOARD;
case "Mouse":
return MOUSE;
case "GPU":
return GPU;
case "Memory":
return MEMORY;
case "Storage":
return STORAGE;
default:
return NULL ;
}
}
}
I pass a list of ComputerComponent class to the averagePrice() function which has two fields of price
which is of type double and
category which is of type String.
My list has 4 elements with categories as "CPU", "Mouse",
"Keyboard" and "Storage" with their respective prices as 34.0, 155.0, 23.0 and 75.0.
When I try to use inEnum(), isInEnum() or EnumUtils.isValidEnum() functions, I get the average price
as 34.0 which I think that they just return the price of the first element rather than the average.
But when I do filtering using
.filter( line ->
line.getCategory().contains("CPU")
|| line.getCategory().contains("GPU")
|| line.getCategory().contains("Monitor")
|| line.getCategory().contains("Keyboard")
|| line.getCategory().contains("Mouse")
|| line.getCategory().contains("Storage")
|| line.getCategory().contains("Memory"))
I get the correct average value of 71.75.
The implementations that I have used for isInEnum() and inEnum() functions are the following:
public static <E extends Enum<E>> boolean isInEnum(String value, Class<E> enumClass) {
for (E e : enumClass.getEnumConstants()) {
if(e.name().contains(value)) { return true; }
}
return false;
}
public static boolean inEnum ( String category, Set<String> value ) {
for(String s: value ) {
if ( category.contains(s) ) {
return true ;
}
}
return false ;
}
How can I use enums correctly with java streams to filter by valid category names and get the correct
average value of price?
What mistake I am making when using streams and its functions ?
You could simply use your ComputerCategoryValue.getValue method and check for null, given the category of line:
public class EnumTest {
#Test
public void testBothMethods() {
final ComputerComponent c1 = new ComputerComponent(ComputerComponentCategory.CPU.getLabel(), 12.21);
final ComputerComponent c2 = new ComputerComponent(ComputerComponentCategory.MEMORY.getLabel(), 23.45);
final List<ComputerComponent> list = Arrays.asList(c1, c2);
assertEquals(averagePriceWithFilter(list), averagePriceWithInEnum(list), 0.01);
}
protected static double averagePriceWithFilter(final List<ComputerComponent> list) {
return list.stream()
.filter(line -> line.getCategory().contains("CPU")
|| line.getCategory().contains("GPU")
|| line.getCategory().contains("Monitor")
|| line.getCategory().contains("Keyboard")
|| line.getCategory().contains("Mouse")
|| line.getCategory().contains("Storage")
|| line.getCategory().contains("Memory"))
.mapToDouble(ComputerComponent::getPrice)
.average()
.orElseThrow(NoSuchElementException::new);
}
protected static double averagePriceWithInEnum(final List<ComputerComponent> list) {
return list.stream()
.filter(line -> ComputerComponentCategory.getValue(line.getCategory()) != null)
.mapToDouble(ComputerComponent::getPrice)
.average()
.orElseThrow(NoSuchElementException::new);
}
}
EDIT: explaining your mistakes:
EnumUtils.getEnumMap(ComputerComponentCategory.class).keySet()) returns a map of the enum name (not its label), so that the check will only work for CPU as there name and label are the same.
Same for the other method!
You need to use getLabel() instead of name() or use equalsIgnoreCase instead of contains.

Spring Boot & JPA: CriterialBuilder check if String contains Expression<String>

1.) I want to get an Expression using an Expression and a String Pattern.
It should become true if the Expression is included in the String Pattern not the other way.
The string pattern can look like "MFD" or "MF" or any other combination of the three letters M,F,D. The Expression is either M, F or D.
So far I have the following code, but i think there is a better solution.
public class AthleteSpecification implements Specification<Athlete> {
private String gender;
public AthleteSpecification(String gender) {
super();
this.gender = gender;
}
#Override
public Predicate toPredicate(Root<Athlete> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate p = cb.disjunction();
if (this.gender == null) {
return cb.conjunction();
} else {
Expression<Boolean> isMaleOrFemaleorDivers;
if (this.gender != null) {
// This is a working solution but not nice
Expression<Boolean> isMale = cb.equal(root.get("gender"), gender.contains("M") ? "M" : "");
Expression<Boolean> isMaleOrFemale = cb.or(isMale,
cb.equal(root.get("gender"), gender.contains("F") ? "F" : ""));
isMaleOrFemaleorDivers = cb.or(isMaleOrFemale,
cb.equal(root.get("gender"), gender.contains("D") ? "D" : ""));
// This is not a solution because i think it only checks if the String is in the Expression<String> not the other way
// Expression<Integer> test = cb.locate(root.get("gender"), gender);
// isMaleOrFemaleorDivers = cb.greaterThan(test, 0);
} else {
isMaleOrFemaleorDivers = cb.conjunction();
}
p.getExpressions().add(cb.and(isNameMatching, isMaleOrFemaleorDivers));
return p;
}
}
}

Spring JPA Specifications Not In Query

Here the IN criteria query using Spring JPA Specification is working fine. but I don't know how to use "NOT IN"..
So How can i use the NOT IN criteria query using Spring JPA Specifications.
SearchSpecification<User> spec = new CommonSpecification<Case>(new SearchCriteria("client.id", Operator.NOT_IN, clientIdList));
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Predicate predicate = null;
switch (searchCriteria.getOperator()) {
case IN:
if (searchCriteria.getValue() instanceof List<?>) {
predicate = getFieldPath(searchCriteria.getKey(), root)
.in(((List<?>) searchCriteria.getValue()).toArray());
}
break;
case NOT_IN:
//What to do???
break;
default:
break;
}
return predicate;
}
private Path<Object> getFieldPath(String key, Root<T> root) {
Path<Object> fieldPath = null;
if (key.contains(".")) {
String[] fields = key.split("\\.");
for (String field : fields) {
if (fieldPath == null) {
fieldPath = root.get(field);
} else {
fieldPath = fieldPath.get(field);
}
}
} else {
fieldPath = root.get(key);
}
return fieldPath;
}
Use the Predicate.not() method; the code is the same as in the IN case, just add .not():
case NOT_IN:
if (searchCriteria.getValue() instanceof List<?>) {
predicate = getFieldPath(searchCriteria.getKey(), root)
.in(((List<?>) searchCriteria.getValue()).toArray())
.not();
}
break;
This functionality also works fine..
case NOT_IN:
if (searchCriteria.getValue() instanceof List<?>) {
predicate = criteriaBuilder.not(getFieldPath(searchCriteria.getKey(), root).in(((List<?>) searchCriteria.getValue()).toArray()));
}
break;

Trying to save comma-separated list

Trying to save selections from a CheckBoxList as a comma-separated list (string) in DB (one or more choices selected). I am using a proxy in order to save as a string because otherwise I'd have to create separate tables in the DB for a relation - the work is not worth it for this simple scenario and I was hoping that I could just convert it to a string and avoid that.
The CheckBoxList uses an enum for it's choices:
public enum Selection
{
Selection1,
Selection2,
Selection3
}
Not to be convoluted, but I use [Display(Name="Choice 1")] and an extension class to display something friendly on the UI. Not sure if I can save that string instead of just the enum, although I think if I save as enum it's not a big deal for me to "display" the friendly string on UI on some confirmation page.
This is the "Record" class that saves a string in the DB:
public virtual string MyCheckBox { get; set; }
This is the "Proxy", which is some sample I found but not directly dealing with enum, and which uses IEnumerable<string> (or should it be IEnumerable<Selection>?):
public IEnumerable<string> MyCheckBox
{
get
{
if (String.IsNullOrWhiteSpace(Record.MyCheckBox)) return new string[] { };
return Record
.MyCheckBox
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(r => r.Trim())
.Where(r => !String.IsNullOrEmpty(r));
}
set
{
Record.MyCheckBox = value == null ? null : String.Join(",", value);
}
}
To save in the DB, I am trying to do this in a create class:
proxy.MyCheckBox = record.MyCheckBox; //getting error here
but am getting the error:
Cannot implicitly convert 'string' to System.Collections.Generic.IEnumerable'
I don't know, if it's possible or better, to use Parse or ToString from the API for enum values.
I know that doing something like this will store whatever I put in the ("") into the DB, so it's just a matter of figuring out how to overcome the error (or, if there is an alternative):
proxy.MyCheckBox = new[] {"foo", "bar"};
I am not good with this stuff and have just been digging and digging to come up with a solution. Any help is much appreciated.
You can accomplish this using a custom user type. The example below uses an ISet<string> on the class and stores the values as a delimited string.
[Serializable]
public class CommaDelimitedSet : IUserType
{
const string delimiter = ",";
#region IUserType Members
public new bool Equals(object x, object y)
{
if (ReferenceEquals(x, y))
{
return true;
}
var xSet = x as ISet<string>;
var ySet = y as ISet<string>;
if (xSet == null || ySet == null)
{
return false;
}
// compare set contents
return xSet.Except(ySet).Count() == 0 && ySet.Except(xSet).Count() == 0;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
var outValue = NHibernateUtil.String.NullSafeGet(rs, names[0]) as string;
if (string.IsNullOrEmpty(outValue))
{
return new HashSet<string>();
}
else
{
var splitArray = outValue.Split(new[] {Delimiter}, StringSplitOptions.RemoveEmptyEntries);
return new HashSet<string>(splitArray);
}
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var inValue = value as ISet<string>;
object setValue = inValue == null ? null : string.Join(Delimiter, inValue);
NHibernateUtil.String.NullSafeSet(cmd, setValue, index);
}
public object DeepCopy(object value)
{
// return new ISet so that Equals can work
// see http://www.mail-archive.com/nhusers#googlegroups.com/msg11054.html
var set = value as ISet<string>;
if (set == null)
{
return null;
}
return new HashSet<string>(set);
}
public object Replace(object original, object target, object owner)
{
return original;
}
public object Assemble(object cached, object owner)
{
return DeepCopy(cached);
}
public object Disassemble(object value)
{
return DeepCopy(value);
}
public SqlType[] SqlTypes
{
get { return new[] {new SqlType(DbType.String)}; }
}
public Type ReturnedType
{
get { return typeof(ISet<string>); }
}
public bool IsMutable
{
get { return false; }
}
#endregion
}
Usage in mapping file:
Map(x => x.CheckboxValues.CustomType<CommaDelimitedSet>();

EmitMapper and List

It's the first time that I use EmitMapper.
I have a list of object ex: Customer and I would like to map this list in a ienumerable of CustomerDTO how can I do that?
Tnx
It's straightforward if you have a list and want to convert it to list of DTOs:
var mapper = ObjectMapperManager.DefaultInstance.GetMapper<Customer, CustomerDTO>();
IEnumerable<CustomerDTO> dtos = listOfCustomer.Select(mapper.map);
The preblem is when the list is in another object, for example User and UserDTO:
class User {
public List<Customer> Customers { get; set; }
}
class UserDTO {
public IEnumerable<CustomerDTO> Customers { get; set; }
}
It seems that EmitMapper does not support conversion from List to Enumerable. A way to support it would be:
var customerMapper = ObjectMapperManager
.DefaultInstance.GetMapper<Customer, CustomerDTO>();
var mapper = ObjectMapperManager.DefaultInstance
.GetMapper<User, UserDTO>(
new DefaultMapConfig()
.ConvertUsing<List<Customer>, IEnumerable<CustomerDTO>>(
a => a.Select(customerMapper.Map))
);
This can be done creating a custom class, implementing the interface "ICustomConverterProvider" and adding a ConvertGeneric to the "DefaultMapConfig".
Looking on the source code of EmitMapper, i found a class named "ArraysConverterProvider", which is the default generic converter from ICollections to Arrays.
Adapting the code from this class to work with IEnumerable collections:
class GenericIEnumerableConverterProvider : ICustomConverterProvider
{
public CustomConverterDescriptor GetCustomConverterDescr(
Type from,
Type to,
MapConfigBaseImpl mappingConfig)
{
var tFromTypeArgs = DefaultCustomConverterProvider.GetGenericArguments(from);
var tToTypeArgs = DefaultCustomConverterProvider.GetGenericArguments(to);
if (tFromTypeArgs == null || tToTypeArgs == null || tFromTypeArgs.Length != 1 || tToTypeArgs.Length != 1)
{
return null;
}
var tFrom = tFromTypeArgs[0];
var tTo = tToTypeArgs[0];
if (tFrom == tTo && (tFrom.IsValueType || mappingConfig.GetRootMappingOperation(tFrom, tTo).ShallowCopy))
{
return new CustomConverterDescriptor
{
ConversionMethodName = "Convert",
ConverterImplementation = typeof(GenericIEnumerableConverter_OneTypes<>),
ConverterClassTypeArguments = new[] { tFrom }
};
}
return new CustomConverterDescriptor
{
ConversionMethodName = "Convert",
ConverterImplementation = typeof(GenericIEnumerableConverter_DifferentTypes<,>),
ConverterClassTypeArguments = new[] { tFrom, tTo }
};
}
}
class GenericIEnumerableConverter_DifferentTypes<TFrom, TTo> : ICustomConverter
{
private Func<TFrom, TTo> _converter;
public IEnumerable<TTo> Convert(IEnumerable<TFrom> from, object state)
{
if (from == null)
{
return null;
}
TTo[] result = new TTo[from.Count()];
int idx = 0;
foreach (var f in from)
{
result[idx++] = _converter(f);
}
return result;
}
public void Initialize(Type from, Type to, MapConfigBaseImpl mappingConfig)
{
var staticConverters = mappingConfig.GetStaticConvertersManager() ?? StaticConvertersManager.DefaultInstance;
var staticConverterMethod = staticConverters.GetStaticConverter(typeof(TFrom), typeof(TTo));
if (staticConverterMethod != null)
{
_converter = (Func<TFrom, TTo>)Delegate.CreateDelegate(
typeof(Func<TFrom, TTo>),
null,
staticConverterMethod
);
}
else
{
_subMapper = ObjectMapperManager.DefaultInstance.GetMapperImpl(typeof(TFrom), typeof(TTo), mappingConfig);
_converter = ConverterBySubmapper;
}
}
ObjectsMapperBaseImpl _subMapper;
private TTo ConverterBySubmapper(TFrom from)
{
return (TTo)_subMapper.Map(from);
}
}
class GenericIEnumerableConverter_OneTypes<T>
{
public IEnumerable<T> Convert(IEnumerable<T> from, object state)
{
if (from == null)
{
return null;
}
return from;
}
}
This code is just a copy with a minimum of adaptation as possible and can be applyed to objects with many levels of hierarchy.
You can use the above code with the following command:
new DefaultMapConfig().ConvertGeneric(
typeof(IEnumerable<>),
typeof(IEnumerable<>),
new GenericIEnumerableConverterProvider());
This saved my day and I hope to save yours too! hehehe

Resources