diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index dc38f50be4f..51590b51910 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -52,6 +52,7 @@ import org.apache.fineract.client.services.ClientApi; import org.apache.fineract.client.services.ClientChargesApi; import org.apache.fineract.client.services.ClientIdentifierApi; +import org.apache.fineract.client.services.ClientSearchV2Api; import org.apache.fineract.client.services.ClientTransactionApi; import org.apache.fineract.client.services.ClientsAddressApi; import org.apache.fineract.client.services.CodeValuesApi; @@ -153,7 +154,7 @@ import retrofit2.converter.scalars.ScalarsConverterFactory; /** - * Fineract Client Java SDK API entry point. Use this instead of the {@link ApiClient}. + * Fineract Client Java SDK API entry point. * * @author Michael Vorburger.ch */ @@ -187,6 +188,8 @@ public final class FineractClient { public final CentersApi centers; public final ChargesApi charges; public final ClientApi clients; + + public final ClientSearchV2Api clientSearchV2; public final ClientChargesApi clientCharges; public final ClientIdentifierApi clientIdentifiers; public final ClientsAddressApi clientAddresses; @@ -305,6 +308,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) { centers = retrofit.create(CentersApi.class); charges = retrofit.create(ChargesApi.class); clients = retrofit.create(ClientApi.class); + clientSearchV2 = retrofit.create(ClientSearchV2Api.class); clientCharges = retrofit.create(ClientChargesApi.class); clientIdentifiers = retrofit.create(ClientIdentifierApi.class); clientAddresses = retrofit.create(ClientsAddressApi.class); @@ -537,7 +541,6 @@ public FineractClient build() { * Obtain the internal Retrofit Builder. This method is typically not required to be invoked for simple API * usages, but can be a handy back door for non-trivial advanced customizations of the API client. * - * @return the {@link ApiClient} which {@link #build()} will use. */ public retrofit2.Retrofit.Builder getRetrofitBuilder() { return retrofitBuilder; @@ -547,7 +550,6 @@ public retrofit2.Retrofit.Builder getRetrofitBuilder() { * Obtain the internal OkHttp Builder. This method is typically not required to be invoked for simple API * usages, but can be a handy back door for non-trivial advanced customizations of the API client. * - * @return the {@link ApiClient} which {@link #build()} will use. */ public okhttp3.OkHttpClient.Builder getOkBuilder() { return okBuilder; diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java b/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java index c478a7bb5d7..cfbc15cbf0b 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/JSON.java @@ -37,6 +37,8 @@ import java.util.Date; import okhttp3.RequestBody; import okhttp3.ResponseBody; +import org.apache.fineract.client.models.ExternalId; +import org.apache.fineract.client.util.adapter.ExternalIdAdapter; import retrofit2.Converter; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; @@ -51,12 +53,14 @@ public class JSON { private final SqlDateTypeAdapter sqlDateTypeAdapter = new SqlDateTypeAdapter(); private final OffsetDateTimeTypeAdapter offsetDateTimeTypeAdapter = new OffsetDateTimeTypeAdapter(); private final LocalDateTypeAdapter localDateTypeAdapter = new LocalDateTypeAdapter(); + private final ExternalIdAdapter externalIdAdapter = new ExternalIdAdapter(); public JSON() { gson = new GsonFireBuilder().createGsonBuilder().registerTypeAdapter(Date.class, dateTypeAdapter) .registerTypeAdapter(java.sql.Date.class, sqlDateTypeAdapter) .registerTypeAdapter(OffsetDateTime.class, offsetDateTimeTypeAdapter) - .registerTypeAdapter(LocalDate.class, localDateTypeAdapter).create(); + .registerTypeAdapter(LocalDate.class, localDateTypeAdapter).registerTypeAdapter(ExternalId.class, externalIdAdapter) + .create(); } public Gson getGson() { diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.java b/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.java new file mode 100644 index 00000000000..284c7f69cc3 --- /dev/null +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/adapter/ExternalIdAdapter.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.client.util.adapter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import org.apache.fineract.client.models.ExternalId; + +public class ExternalIdAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, ExternalId value) throws IOException { + if (value != null && Boolean.FALSE.equals(value.getEmpty())) { + out.value(value.getValue()); + } else { + out.nullValue(); + } + } + + @Override + public ExternalId read(JsonReader in) throws IOException { + ExternalId result = new ExternalId().empty(true); + switch (in.peek()) { + case NULL: + in.nextNull(); + return result; + default: + String value = in.nextString(); + return new ExternalId().empty(false).value(value); + } + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java new file mode 100644 index 00000000000..c617b7364f6 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/jpa/CriteriaQueryFactory.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jpa; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; + +@Component +public class CriteriaQueryFactory { + + public List fromPageable(Pageable pageable, CriteriaBuilder cb, Root root) { + return fromPageable(pageable, cb, root, () -> null); + } + + public List fromPageable(Pageable pageable, CriteriaBuilder cb, Root root, Supplier defaultOrderSupplier) { + List orders = new ArrayList<>(); + Sort sort = pageable.getSort(); + if (sort.isSorted()) { + for (Sort.Order order : sort) { + if (order.isAscending()) { + orders.add(cb.asc(root.get(order.getProperty()))); + } else { + orders.add(cb.desc(root.get(order.getProperty()))); + } + } + } else { + Order defaultOrder = defaultOrderSupplier.get(); + if (defaultOrder != null) { + orders.add(defaultOrder); + } + } + return orders; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java new file mode 100644 index 00000000000..fd9f7b56f8b --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/PagedRequest.java @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.service; + +import static org.apache.commons.collections4.CollectionUtils.isEmpty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.Data; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +@Data +public class PagedRequest { + + public static final int DEFAULT_PAGE_SIZE = 50; + + private T request; + + private int page; + private int size = DEFAULT_PAGE_SIZE; + + private List sorts = new ArrayList<>(); + + public Optional getRequest() { + return Optional.ofNullable(request); + } + + public Pageable toPageable() { + if (isEmpty(sorts)) { + return PageRequest.of(page, size); + } else { + List orders = sorts.stream().map(SortOrder::toOrder).toList(); + return PageRequest.of(page, size, Sort.by(orders)); + } + } + + @Data + @SuppressWarnings({ "unused" }) + private static class SortOrder { + + private Direction direction; + private String property; + + private enum Direction { + ASC, DESC; + } + + private Sort.Order toOrder() { + Sort.Direction d = Sort.Direction.fromString(direction.name()); + return new Sort.Order(d, property); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java similarity index 96% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java rename to fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java index bf45047886d..90641f22d83 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/JerseyConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyConfig.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fineract.infrastructure.core.config; +package org.apache.fineract.infrastructure.core.jersey; import javax.annotation.PostConstruct; import javax.ws.rs.ApplicationPath; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java new file mode 100644 index 00000000000..35683df5ab5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonConverterConfig.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey; + +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter; +import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonDeserializerAdapter; +import org.apache.fineract.infrastructure.core.jersey.serializer.JacksonSerializerAdapter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + +@Configuration +public class JerseyJacksonConverterConfig { + + @Bean + public MappingJackson2HttpMessageConverter jacksonHttpConverter(List> serializers, + List> deserializers, List> jsonConverters) { + List> mergedSerializers = new ArrayList<>(serializers); + mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList()); + + List> mergedDeserializers = new ArrayList<>(deserializers); + mergedDeserializers.addAll(jsonConverters.stream().map(JacksonDeserializerAdapter::new).toList()); + + return new MappingJackson2HttpMessageConverter(new Jackson2ObjectMapperBuilder().indentOutput(true) + .serializers(mergedSerializers.toArray(new JsonSerializer[0])) + .deserializers(mergedDeserializers.toArray(new JsonDeserializer[0])) + .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS).modulesToInstall(new ParameterNamesModule()).build()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java new file mode 100644 index 00000000000..6d57d23d122 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/JerseyJacksonObjectArgumentHandler.java @@ -0,0 +1,116 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonInputMessage; +import org.springframework.stereotype.Component; + +@Provider +@Produces(MediaType.APPLICATION_JSON_VALUE) +@Consumes(MediaType.APPLICATION_JSON_VALUE) +@Component +@RequiredArgsConstructor +public class JerseyJacksonObjectArgumentHandler implements MessageBodyReader, MessageBodyWriter { + + private final MappingJackson2HttpMessageConverter converter; + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType) { + return true; + } + + @Override + @SuppressWarnings({ "unchecked" }) + public T readFrom(Class type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + if (String.class == genericType) { + // If the request type is String, keep it that way. + StringWriter writer = new StringWriter(); + IOUtils.copy(entityStream, writer, UTF_8); + String json = writer.toString(); + return type.cast(json); + } else { + // Create the proper type from the JSON + HttpHeaders headers = new HttpHeaders(); + headers.putAll(httpHeaders); + return (T) converter.read(genericType, type, new MappingJacksonInputMessage(entityStream, headers)); + } + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType) { + return true; + } + + @Override + public void writeTo(T t, Class type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + if (String.class == genericType) { + // If the response type is String, keep it that way. + IOUtils.write((String) t, entityStream, UTF_8); + } else { + // Create the proper JSON string from the object + HttpHeaders headers = new HttpHeaders(); + httpHeaders.forEach((header, rawValues) -> { + List values = rawValues.stream().map(Object::toString).toList(); + headers.put(header, values); + }); + converter.write(t, genericType, MediaType.APPLICATION_JSON, new SimpleHttpOutputMessage(entityStream, headers)); + } + } + + @RequiredArgsConstructor + private static class SimpleHttpOutputMessage implements HttpOutputMessage { + + private final OutputStream outputStream; + private final HttpHeaders headers; + + @Override + public OutputStream getBody() throws IOException { + return outputStream; + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java new file mode 100644 index 00000000000..080905ea209 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/DateJsonConverter.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import org.springframework.stereotype.Component; + +@Component +public class DateJsonConverter implements JsonConverter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_INSTANT; + + @Override + public Date convertToObject(JsonParser parser) throws IOException { + Date result = null; + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String formattedDate = parser.getText(); + result = Date.from(Instant.from(FORMATTER.parse(formattedDate))); + } + return result; + } + + @Override + public void convertToJson(Date value, JsonGenerator generator) throws IOException { + if (value != null) { + generator.writeString(FORMATTER.format(value.toInstant())); + } + } + + @Override + public Class convertedType() { + return Date.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java new file mode 100644 index 00000000000..b8e089d6525 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/ExternalIdJsonConverter.java @@ -0,0 +1,52 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.springframework.stereotype.Component; + +@Component +public class ExternalIdJsonConverter implements JsonConverter { + + @Override + public ExternalId convertToObject(JsonParser parser) throws IOException { + ExternalId result = ExternalId.empty(); + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String externalId = parser.getText(); + result = new ExternalId(externalId); + } + return result; + } + + @Override + public void convertToJson(ExternalId value, JsonGenerator generator) throws IOException { + if (value != null && !value.isEmpty()) { + generator.writeString(value.getValue()); + } + } + + @Override + public Class convertedType() { + return ExternalId.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java new file mode 100644 index 00000000000..daad86f0093 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/JsonConverter.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import java.io.IOException; + +public interface JsonConverter { + + T convertToObject(JsonParser parser) throws IOException; + + void convertToJson(T value, JsonGenerator generator) throws IOException; + + Class convertedType(); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java new file mode 100644 index 00000000000..afb264d54fa --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateJsonConverter.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; + +@Component +public class LocalDateJsonConverter implements JsonConverter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE; + + @Override + public LocalDate convertToObject(JsonParser parser) throws IOException { + LocalDate result = null; + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String formattedDate = parser.getText(); + result = LocalDate.parse(formattedDate, FORMATTER); + } + return result; + } + + @Override + public void convertToJson(LocalDate value, JsonGenerator generator) throws IOException { + if (value != null) { + generator.writeString(FORMATTER.format(value)); + } + } + + @Override + public Class convertedType() { + return LocalDate.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java new file mode 100644 index 00000000000..6c9f2a2846e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalDateTimeJsonConverter.java @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; + +@Component +public class LocalDateTimeJsonConverter implements JsonConverter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + @Override + public LocalDateTime convertToObject(JsonParser parser) throws IOException { + LocalDateTime result = null; + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String formattedDate = parser.getText(); + result = LocalDateTime.parse(formattedDate, FORMATTER); + } + return result; + } + + @Override + public void convertToJson(LocalDateTime value, JsonGenerator generator) throws IOException { + if (value != null) { + generator.writeString(FORMATTER.format(value)); + } + + } + + @Override + public Class convertedType() { + return LocalDateTime.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java new file mode 100644 index 00000000000..20b57480062 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/LocalTimeJsonConverter.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; + +@Component +public class LocalTimeJsonConverter implements JsonConverter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_LOCAL_TIME; + + @Override + public LocalTime convertToObject(JsonParser parser) throws IOException { + LocalTime result = null; + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String formattedDate = parser.getText(); + result = LocalTime.parse(formattedDate, FORMATTER); + } + return result; + } + + @Override + public void convertToJson(LocalTime value, JsonGenerator generator) throws IOException { + if (value != null) { + generator.writeString(FORMATTER.format(value)); + } + } + + @Override + public Class convertedType() { + return LocalTime.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java new file mode 100644 index 00000000000..19b0a905960 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/converter/OffsetDateTimeJsonConverter.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.converter; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.stereotype.Component; + +@Component +public class OffsetDateTimeJsonConverter implements JsonConverter { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + @Override + public OffsetDateTime convertToObject(JsonParser parser) throws IOException { + OffsetDateTime result = null; + if (parser.hasToken(JsonToken.VALUE_STRING)) { + String formattedDate = parser.getText(); + result = OffsetDateTime.parse(formattedDate, FORMATTER); + } + return result; + } + + @Override + public void convertToJson(OffsetDateTime value, JsonGenerator generator) throws IOException { + if (value != null) { + generator.writeString(FORMATTER.format(value)); + } + } + + @Override + public Class convertedType() { + return OffsetDateTime.class; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java new file mode 100644 index 00000000000..18ef095ae4a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonDeserializerAdapter.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.serializer; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter; + +@RequiredArgsConstructor +public class JacksonDeserializerAdapter extends JsonDeserializer { + + private final JsonConverter converter; + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + return converter.convertToObject(p); + } + + @Override + public Class handledType() { + return converter.convertedType(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java new file mode 100644 index 00000000000..951a0872eb2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/jersey/serializer/JacksonSerializerAdapter.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.jersey.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.jersey.converter.JsonConverter; + +@RequiredArgsConstructor +public class JacksonSerializerAdapter extends JsonSerializer { + + private final JsonConverter converter; + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + converter.convertToJson(value, gen); + } + + @Override + public Class handledType() { + return converter.convertedType(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java new file mode 100644 index 00000000000..b45527c5395 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2Api.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.api.v2.search; + +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData; +import org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch; +import org.springframework.data.domain.Page; + +public interface ClientSearchV2Api { + + Page searchByText(PagedRequest request); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.java new file mode 100644 index 00000000000..780c1eb3577 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiDelegate.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.api.v2.search; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.portfolio.client.service.search.ClientSearchService; +import org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData; +import org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ClientSearchV2ApiDelegate implements ClientSearchV2Api { + + private final ClientSearchService searchService; + + @Override + public Page searchByText(PagedRequest request) { + return searchService.searchByText(request); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java new file mode 100644 index 00000000000..19ff0692500 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/v2/search/ClientSearchV2ApiResource.java @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.api.v2.search; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData; +import org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +@Path("/v2/clients") +@Component +@Tag(name = "ClientSearchV2") +@RequiredArgsConstructor +public class ClientSearchV2ApiResource implements ClientSearchV2Api { + + private final ClientSearchV2ApiDelegate delegate; + + @Override + @POST + @Path("search") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Search Clients by text") + public Page searchByText(@Parameter PagedRequest request) { + return delegate.searchByText(request); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java index 7d09ec67c05..e6db6660660 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientRepository.java @@ -19,12 +19,13 @@ package org.apache.fineract.portfolio.client.domain; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.client.domain.search.SearchingClientRepository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -interface ClientRepository extends JpaRepository, JpaSpecificationExecutor { +public interface ClientRepository extends JpaRepository, JpaSpecificationExecutor, SearchingClientRepository { String FIND_CLIENT_BY_ACCOUNT_NUMBER = "select client from Client client where client.accountNumber = :accountNumber"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java new file mode 100644 index 00000000000..93da80ce9f6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchedClient.java @@ -0,0 +1,41 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.domain.search; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +@Getter +@RequiredArgsConstructor +public class SearchedClient { + + private final Long id; + private final String displayName; + private final ExternalId externalId; + private final String accountNo; + private final Long officeId; + private final String officeName; + private final String mobileNo; + private final Integer status; + private final LocalDate activationDate; + private final OffsetDateTime createdDate; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.java new file mode 100644 index 00000000000..e1827da1b4d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepository.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.domain.search; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface SearchingClientRepository { + + Page searchByText(String searchText, Pageable pageable, String officeHierarchy); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java new file mode 100644 index 00000000000..a6d6d3a42e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/search/SearchingClientRepositoryImpl.java @@ -0,0 +1,82 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.domain.search; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.jpa.CriteriaQueryFactory; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.client.domain.Client; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class SearchingClientRepositoryImpl implements SearchingClientRepository { + + private final EntityManager entityManager; + private final CriteriaQueryFactory criteriaQueryFactory; + + @Override + public Page searchByText(String searchText, Pageable pageable, String officeHierarchy) { + /* + * this whole thing can be replaced with Spring Data JPA 3+ with a findBy(Specification, Pageable) call but at + * this point the upgrade is too costly + * + * https://github.com/spring-projects/spring-data-jpa/issues/2499 + */ + String hierarchyLikeValue = officeHierarchy + "%"; + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(SearchedClient.class); + Root root = query.from(Client.class); + Path office = root.get("office"); + + query.select(cb.construct(SearchedClient.class, root.get("id"), root.get("displayName"), root.get("externalId"), + root.get("accountNumber"), office.get("id"), office.get("name"), root.get("mobileNo"), root.get("status"), + root.get("activationDate"), root.get("createdDate"))); + + List predicates = new ArrayList<>(); + predicates.add(cb.like(office.get("hierarchy"), hierarchyLikeValue)); + + String searchLikeValue = "%" + searchText + "%"; + predicates.add(cb.or(cb.like(root.get("accountNumber"), searchLikeValue), cb.like(root.get("displayName"), searchLikeValue), + cb.like(root.get("externalId"), searchLikeValue), cb.like(root.get("mobileNo"), searchLikeValue))); + + query.where(cb.and(predicates.toArray(new Predicate[0]))); + + List orders = criteriaQueryFactory.fromPageable(pageable, cb, root, () -> cb.desc(root.get("id"))); + query.orderBy(orders); + + List result = entityManager.createQuery(query).setFirstResult(pageable.getPageNumber()) + .setMaxResults(pageable.getPageSize()).getResultList(); + + return new PageImpl<>(result, pageable, result.size()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java new file mode 100644 index 00000000000..751e78c65db --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/ClientSearchService.java @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.service.search; + +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.service.PagedRequest; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.domain.ClientRepository; +import org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData; +import org.apache.fineract.portfolio.client.service.search.domain.ClientTextSearch; +import org.apache.fineract.portfolio.client.service.search.mapper.ClientSearchDataMapper; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ClientSearchService { + + private final PlatformSecurityContext context; + private final ClientRepository clientRepository; + private final ClientSearchDataMapper clientSearchDataMapper; + + public Page searchByText(PagedRequest searchRequest) { + validateTextSearchRequest(searchRequest); + return executeTextSearch(searchRequest); + } + + private void validateTextSearchRequest(PagedRequest searchRequest) { + Objects.requireNonNull(searchRequest, "searchRequest must not be null"); + + context.isAuthenticated(); + } + + private Page executeTextSearch(PagedRequest searchRequest) { + final String hierarchy = context.authenticatedUser().getOffice().getHierarchy(); + + Optional request = searchRequest.getRequest(); + String requestSearchText = request.map(ClientTextSearch::getText).orElse(null); + String searchText = StringUtils.defaultString(requestSearchText, ""); + + Pageable pageable = searchRequest.toPageable(); + + return clientRepository.searchByText(searchText, pageable, hierarchy).map(clientSearchDataMapper::map); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java new file mode 100644 index 00000000000..dac1637bc79 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientSearchData.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.service.search.domain; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.Data; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +@Data +public class ClientSearchData { + + private Long id; + private String displayName; + private ExternalId externalId; + private String accountNo; + private Long officeId; + private String officeName; + private String mobileNo; + private EnumOptionData status; + private LocalDate activationDate; + private OffsetDateTime createdDate; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.java new file mode 100644 index 00000000000..f6523aaffa6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/domain/ClientTextSearch.java @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.service.search.domain; + +import lombok.Data; + +@Data +public class ClientTextSearch { + + private String text; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java new file mode 100644 index 00000000000..2085db8619e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/search/mapper/ClientSearchDataMapper.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.client.service.search.mapper; + +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.portfolio.client.domain.ClientEnumerations; +import org.apache.fineract.portfolio.client.domain.search.SearchedClient; +import org.apache.fineract.portfolio.client.service.search.domain.ClientSearchData; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapstructMapperConfig.class) +public interface ClientSearchDataMapper { + + @Mapping(target = "status", source = "source", qualifiedByName = "toStatus") + ClientSearchData map(SearchedClient source); + + @Named("toStatus") + default EnumOptionData toStatus(SearchedClient client) { + return ClientEnumerations.status(client.getStatus()); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java index d8ea651b2d9..b6d5e7c7b65 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/config/ApiVerificationTest.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import javax.ws.rs.Path; import org.apache.fineract.AbstractSpringTest; +import org.apache.fineract.infrastructure.core.jersey.JerseyConfig; import org.assertj.core.api.SoftAssertions; import org.glassfish.jersey.server.model.Resource; import org.junit.jupiter.api.Test; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java new file mode 100644 index 00000000000..8a9c982c0e9 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ClientSearchTest.java @@ -0,0 +1,196 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import org.apache.fineract.client.models.GetClientsClientIdResponse; +import org.apache.fineract.client.models.PageClientSearchData; +import org.apache.fineract.client.models.PostClientsRequest; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.SortOrder; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ClientSearchTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private ClientHelper clientHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + clientHelper = new ClientHelper(requestSpec, responseSpec); + } + + @Test + public void testClientSearchWorks_WithLastnameTextOnDefaultOrdering() { + // given + String lastname = Utils.randomStringGenerator("Client_LastName_", 5); + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + request1.setLastname(lastname); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + request2.setLastname(lastname); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + request3.setLastname(lastname); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(lastname); + // then + assertThat(result.getTotalElements()).isEqualTo(3); + assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request3.getExternalId()); + assertThat(result.getContent().get(1).getExternalId().getValue()).isEqualTo(request2.getExternalId()); + assertThat(result.getContent().get(2).getExternalId().getValue()).isEqualTo(request1.getExternalId()); + } + + @Test + public void testClientSearchWorks_WithLastnameText_OrderedByIdAsc() { + // given + String lastname = Utils.randomStringGenerator("Client_LastName_", 5); + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + request1.setLastname(lastname); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + request2.setLastname(lastname); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + request3.setLastname(lastname); + clientHelper.createClient(request3); + + SortOrder sortOrder = new SortOrder().property("id").direction(SortOrder.DirectionEnum.ASC); + // when + PageClientSearchData result = clientHelper.searchClients(lastname, sortOrder); + // then + assertThat(result.getTotalElements()).isEqualTo(3); + assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request1.getExternalId()); + assertThat(result.getContent().get(1).getExternalId().getValue()).isEqualTo(request2.getExternalId()); + assertThat(result.getContent().get(2).getExternalId().getValue()).isEqualTo(request3.getExternalId()); + } + + @Test + public void testClientSearchWorks_ByExternalId() { + // given + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(request2.getExternalId()); + // then + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent().get(0).getExternalId().getValue()).isEqualTo(request2.getExternalId()); + } + + @Test + public void testClientSearchWorks_ByAccountNumber() { + // given + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + PostClientsResponse response2 = clientHelper.createClient(request2); + GetClientsClientIdResponse client2Data = ClientHelper.getClient(requestSpec, responseSpec, + Math.toIntExact(response2.getClientId())); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(client2Data.getAccountNo()); + // then + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent().get(0).getAccountNo()).isEqualTo(client2Data.getAccountNo()); + } + + @Test + public void testClientSearchWorks_ByDisplayName() { + // given + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request2); + String client2DisplayName = "%s %s".formatted(request2.getFirstname(), request2.getLastname()); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(client2DisplayName); + // then + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent().get(0).getDisplayName()).isEqualTo(client2DisplayName); + } + + @Test + public void testClientSearchWorks_ByMobileNo() { + // given + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + request2.setMobileNo(Utils.randomNumberGenerator(8).toString()); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(request2.getMobileNo()); + // then + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent().get(0).getMobileNo()).isEqualTo(request2.getMobileNo()); + } + + @Test + public void testClientSearchDoesntReturnAnything_ByMobileNo() { + // given + PostClientsRequest request1 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request1); + + PostClientsRequest request2 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request2); + + PostClientsRequest request3 = ClientHelper.defaultClientCreationRequest(); + clientHelper.createClient(request3); + // when + PageClientSearchData result = clientHelper.searchClients(Utils.randomNumberGenerator(8).toString()); + // then + assertThat(result.getTotalElements()).isEqualTo(0); + assertThat(result.getContent()).isEmpty(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java index 2659bc33d6c..3ed19480cf7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java @@ -38,18 +38,22 @@ import javax.ws.rs.core.MediaType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.ClientTextSearch; import org.apache.fineract.client.models.DeleteClientsClientIdResponse; import org.apache.fineract.client.models.GetClientClientIdAddressesResponse; import org.apache.fineract.client.models.GetClientTransferProposalDateResponse; import org.apache.fineract.client.models.GetClientsClientIdAccountsResponse; import org.apache.fineract.client.models.GetClientsClientIdResponse; import org.apache.fineract.client.models.GetObligeeData; +import org.apache.fineract.client.models.PageClientSearchData; +import org.apache.fineract.client.models.PagedRequestClientTextSearch; import org.apache.fineract.client.models.PostClientClientIdAddressesRequest; import org.apache.fineract.client.models.PostClientClientIdAddressesResponse; import org.apache.fineract.client.models.PostClientsClientIdResponse; import org.apache.fineract.client.models.PostClientsRequest; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PutClientsClientIdResponse; +import org.apache.fineract.client.models.SortOrder; import org.apache.fineract.client.util.JSON; import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -97,6 +101,27 @@ public PostClientsResponse createClient(final PostClientsRequest request) { return ok(fineract().clients.create6(request)); } + public PageClientSearchData searchClients(String text) { + ClientTextSearch clientTextSearch = new ClientTextSearch(); + clientTextSearch.setText(text); + PagedRequestClientTextSearch request = new PagedRequestClientTextSearch(); + request.setRequest(clientTextSearch); + return searchClients(request); + } + + public PageClientSearchData searchClients(String text, SortOrder sortOrder) { + ClientTextSearch clientTextSearch = new ClientTextSearch(); + clientTextSearch.setText(text); + PagedRequestClientTextSearch request = new PagedRequestClientTextSearch(); + request.setRequest(clientTextSearch); + request.setSorts(List.of(sortOrder)); + return searchClients(request); + } + + public PageClientSearchData searchClients(PagedRequestClientTextSearch request) { + return ok(fineract().clientSearchV2.searchByText(request)); + } + public static PostClientsResponse addClientAsPerson(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String jsonPayload) { final String response = Utils.performServerPost(requestSpec, responseSpec, CREATE_CLIENT_URL, jsonPayload);