FINERACT-1724: Client search V2 API

This commit is contained in:
Arnold Galovics
2023-05-23 13:41:30 +02:00
committed by Arnold Gálovics
parent c18fead7f3
commit 68c6053bd7
31 changed files with 1473 additions and 6 deletions
@@ -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;
@@ -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<JsonSerializer<?>> serializers,
List<JsonDeserializer<?>> deserializers, List<JsonConverter<?>> jsonConverters) {
List<JsonSerializer<?>> mergedSerializers = new ArrayList<>(serializers);
mergedSerializers.addAll(jsonConverters.stream().map(JacksonSerializerAdapter::new).toList());
List<JsonDeserializer<?>> 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());
}
}
@@ -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<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
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<T> type, Type genericType, Annotation[] annotations, javax.ws.rs.core.MediaType mediaType,
MultivaluedMap<String, String> 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<String, Object> 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<String> 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;
}
}
}
@@ -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<Date> {
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<Date> convertedType() {
return Date.class;
}
}
@@ -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<ExternalId> {
@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<ExternalId> convertedType() {
return ExternalId.class;
}
}
@@ -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> {
T convertToObject(JsonParser parser) throws IOException;
void convertToJson(T value, JsonGenerator generator) throws IOException;
Class<T> convertedType();
}
@@ -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<LocalDate> {
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<LocalDate> convertedType() {
return LocalDate.class;
}
}
@@ -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<LocalDateTime> {
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<LocalDateTime> convertedType() {
return LocalDateTime.class;
}
}
@@ -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<LocalTime> {
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<LocalTime> convertedType() {
return LocalTime.class;
}
}
@@ -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<OffsetDateTime> {
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<OffsetDateTime> convertedType() {
return OffsetDateTime.class;
}
}
@@ -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<T> extends JsonDeserializer<T> {
private final JsonConverter<T> converter;
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return converter.convertToObject(p);
}
@Override
public Class<?> handledType() {
return converter.convertedType();
}
}
@@ -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<T> extends JsonSerializer<T> {
private final JsonConverter<T> converter;
@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
converter.convertToJson(value, gen);
}
@Override
public Class<T> handledType() {
return converter.convertedType();
}
}
@@ -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<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> request);
}
@@ -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<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> request) {
return searchService.searchByText(request);
}
}
@@ -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<ClientSearchData> searchByText(@Parameter PagedRequest<ClientTextSearch> request) {
return delegate.searchByText(request);
}
}
@@ -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<Client, Long>, JpaSpecificationExecutor<Client> {
public interface ClientRepository extends JpaRepository<Client, Long>, JpaSpecificationExecutor<Client>, SearchingClientRepository {
String FIND_CLIENT_BY_ACCOUNT_NUMBER = "select client from Client client where client.accountNumber = :accountNumber";
@@ -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;
}
@@ -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<SearchedClient> searchByText(String searchText, Pageable pageable, String officeHierarchy);
}
@@ -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<SearchedClient> 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<SearchedClient> query = cb.createQuery(SearchedClient.class);
Root<Client> root = query.from(Client.class);
Path<Office> 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<Predicate> 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<Order> orders = criteriaQueryFactory.fromPageable(pageable, cb, root, () -> cb.desc(root.get("id")));
query.orderBy(orders);
List<SearchedClient> result = entityManager.createQuery(query).setFirstResult(pageable.getPageNumber())
.setMaxResults(pageable.getPageSize()).getResultList();
return new PageImpl<>(result, pageable, result.size());
}
}
@@ -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<ClientSearchData> searchByText(PagedRequest<ClientTextSearch> searchRequest) {
validateTextSearchRequest(searchRequest);
return executeTextSearch(searchRequest);
}
private void validateTextSearchRequest(PagedRequest<ClientTextSearch> searchRequest) {
Objects.requireNonNull(searchRequest, "searchRequest must not be null");
context.isAuthenticated();
}
private Page<ClientSearchData> executeTextSearch(PagedRequest<ClientTextSearch> searchRequest) {
final String hierarchy = context.authenticatedUser().getOffice().getHierarchy();
Optional<ClientTextSearch> 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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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());
}
}
@@ -22,6 +22,7 @@ import java.util.Set;
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;