public class DsmWebapiResponse<T> {

    private boolean success;
    private T data;
    private DsmWebApiResponseError error;

    @JsonCreator
    public DsmWebapiResponse(@JsonProperty("success") boolean success,
                             @JsonProperty("data") T data,
                             @JsonProperty("error") DsmWebApiResponseError error) {
        this.success = success;
        this.data = data;
        this.error = error;
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public DsmWebApiResponseError getError() {
        return error;
    }

} public class File {

    private final String path;
    private final String name;
    private final boolean directory;
    private final FileProperties properties;
    private final List<File> children = new ArrayList<>();

    @JsonCreator
    public File(
            @JsonProperty("path") String path,
            @JsonProperty("name") String name,
            @JsonProperty("isdir") boolean directory,
            @JsonProperty("additional") FileProperties properties,
            @JsonProperty("children") FileList children) {
        this.path = path;
        this.name = name;
        this.directory = directory;
        this.properties = properties;
        if(children != null) {
            this.children.addAll(children.getElements());
        }
    }

    public String getPath() {
        return path;
    }

    public String getName() {
        return name;
    }

    public boolean isDirectory() {
        return directory;
    }

    public FileProperties getProperties() {
        return properties;
    }

    public List<File> getChildren() {
        return Collections.unmodifiableList(children);
    }

    public static class FileList extends PaginatedList<File> {

        public FileList(@JsonProperty("total") int total,
                        @JsonProperty("offset") int offset,
                        @JsonProperty("files") List<File> files) {
            super(total, offset, files);
        }
    }
} public class PaginationAndSorting {

    public static final PaginationAndSorting DEFAULT_PAGINATION_AND_SORTING = new PaginationAndSorting(0, -1, Sort.NAME, SortDirection.ASC);

    public enum Sort {

        NAME("name"), USER("user"), GROUP("group"), MTIME("mtime"), ATIME("atime"), CTIME("ctime"), CRTIME("crtime"), POSIX("posix"), SIZE("size"), TYPE("type");

        private final String representation;

        Sort(String representation) {
            this.representation = representation;
        }

        public String getRepresentation() {
            return representation;
        }
    }

    public enum SortDirection {

        ASC("asc"), DESC("desc");

        private final String representation;

        SortDirection(String representation) {
            this.representation = representation;
        }

        public String getRepresentation() {
            return representation;
        }
    }

    private final int offset;
    private final int limit;
    private final Sort sortBy;
    private final SortDirection sortDirection;

    public PaginationAndSorting(int offset, int limit, Sort sortBy, SortDirection sortDirection) {
        this.offset = offset;
        this.limit = limit;
        this.sortBy = sortBy;
        this.sortDirection = sortDirection;
    }

    public static PaginationAndSorting getDefaultPaginationAndSorting() {
        return DEFAULT_PAGINATION_AND_SORTING;
    }

    public int getOffset() {
        return offset;
    }

    public int getLimit() {
        return limit;
    }

    public Sort getSortBy() {
        return sortBy;
    }

    public SortDirection getSortDirection() {
        return sortDirection;
    }
} public abstract class AbstractDsmServiceImpl {

    @Autowired
    private ApiInfoService apiInfoService;

    @Autowired
    private DsmWebapiClient dsmWebapiClient;

    private final String apiId;

    private ApiInfo apiInfo;

    public AbstractDsmServiceImpl(String apiId) {
        this.apiId = apiId;
    }

    @PostConstruct
    public final void init() {
        apiInfo = apiInfoService.findOne(apiId);
    }

    public DsmWebapiClient getDsmWebapiClient() {
        return dsmWebapiClient;
    }

    public ApiInfoService getApiInfoService() {
        return apiInfoService;
    }

    public String getApiId() {
        return apiId;
    }

    public ApiInfo getApiInfo() {
        return apiInfo;
    }
} public class DsmWebApiResponseError {

    private final int code;
    private final List<DsmWebApiResponseError> errors = new ArrayList<>();
    private final Map<String, String> details = new HashMap<>();

    @JsonCreator
    public DsmWebApiResponseError(@JsonProperty("code") int code,
                                  @JsonProperty("errors") List<DsmWebApiResponseError> errors) {
        this.code = code;
        this.errors.addAll(ofNullable(errors).orElse(new ArrayList<>()));
    }

    public int getCode() {
        return code;
    }

    public List<DsmWebApiResponseError> getErrors() {
        return Collections.unmodifiableList(errors);
    }

    public Map<String, String> getDetails() {
        return Collections.unmodifiableMap(details);
    }

    @JsonAnySetter
    public void addDetail(String key, String value) {
        this.details.put(key, value);
    }

    public String getDetailValue(String key) {
        return details.get(key);
    }

    @Override
    public String toString() {
        return "DsmWebApiResponseError{" +
                "code=" + code +
                ", errors=" + errors +
                ", details=" + details +
                '}';
    }
} public class DsmWebapiRequest {

    private final String api;
    private final String version;
    private final String path;
    private final String method;
    private final Map<String, String> parameters = new HashMap<>();

    public DsmWebapiRequest(String api, String version, String path, String method) {
        this.api = api;
        this.version = version;
        this.path = path;
        this.method = method;
    }

    public String getApi() {
        return api;
    }

    public String getVersion() {
        return version;
    }

    public String getPath() {
        return path;
    }

    public String getMethod() {
        return method;
    }

    public Map<String, String> getParameters() {
        return Collections.unmodifiableMap(parameters);
    }

    public DsmWebapiRequest parameter(String key, String value) {
        this.parameters.put(key, value);
        return this;
    }

    public DsmWebapiRequest parameter(String key, Object value) {
        this.parameters.put(key, value.toString());
        return this;
    }

    public <T> DsmWebapiRequest optionalParameter(String key, Optional<T> value, Function<T, String> toString) {
        if(value.isPresent()) {
            this.parameter(key, toString.apply(value.get()));
        }
        return this;
    }

    public <T> DsmWebapiRequest optionalParameter(String key, Optional<T> value) {
        return this.optionalParameter(key, value, T::toString);
    }

    public <T> DsmWebapiRequest optionalParameter(String key, T value, Predicate<T> shouldAdd) {
        if(shouldAdd.test(value)) {
            this.parameter(key, value);
        }
        return this;
    }

    public DsmWebapiRequest optionalStringParameter(String key, String value) {
        return this.optionalParameter(key, value, s -> !Strings.isNullOrEmpty(value));
    }
} public enum FileType {
    FILE("file"), DIRECTORY("directory"), ALL("all");

    private final String representation;

    FileType(String representation) {
        this.representation = representation;
    }

    public String getRepresentation() {
        return representation;
    }
} public class TaskId {

    private final String taskId;

    @JsonCreator
    public TaskId(@JsonProperty("taskid") String taskId) {
        this.taskId = taskId;
    }

    public String getTaskId() {
        return taskId;
    }
} public class TaskTimeOutException extends DsmWebApiClientException {

    public TaskTimeOutException(Throwable cause) {
        super("Task did not complete within allowed delay", cause);
    }

    public TaskTimeOutException() {
        super("Task did not complete within allowed delay");
    }
} @Component
public class TimeZoneUtil implements ApplicationContextAware {

    private static TimeZoneUtil instance;

    @Autowired
    private TimeZoneProvider timeZoneProvider;

    public ZoneOffset getZoneOffset() {
        return LocalDateTime.now().atZone(timeZoneProvider.getTimeZone().toZoneId()).getOffset();
    }

    public static ZoneOffset getOffset() {
        return instance.getZoneOffset();
    }

    @Override
    public synchronized void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        instance = applicationContext.getBean(TimeZoneUtil.class);
    }
}
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.gauthierj.dsm.webapi.client.DsmWebapiResponse;
import com.github.gauthierj.dsm.webapi.client.filestation.common.File;
import com.github.gauthierj.dsm.webapi.client.filestation.common.PaginationAndSorting;
import com.google.common.base.Joiner;
import com.jayway.awaitility.core.ConditionTimeoutException;
import com.github.gauthierj.dsm.webapi.client.AbstractDsmServiceImpl;
import com.github.gauthierj.dsm.webapi.client.DsmWebApiResponseError;
import com.github.gauthierj.dsm.webapi.client.DsmWebapiRequest;
import com.github.gauthierj.dsm.webapi.client.filestation.common.FileType;
import com.github.gauthierj.dsm.webapi.client.filestation.common.TaskId;
import com.github.gauthierj.dsm.webapi.client.filestation.exception.TaskTimeOutException;
import com.github.gauthierj.dsm.webapi.client.timezone.TimeZoneUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import static com.jayway.awaitility.Awaitility.await;
package com.github.gauthierj.dsm.webapi.client.filestation.search;




@Component
public class SearchServiceImpl extends AbstractDsmServiceImpl implements SearchService {

    // API Infos
    private static final String API_ID = "SYNO.FileStation.Search";
    private static final String API_VERSION = "1";

    // API Methods
    private static final String METHOD_CLEAN = "clean";
    private static final String METHOD_START = "start";
    private static final String METHOD_STOP = "stop";

    // Parameters
    private static final String PARAMETER_ADDITIONAL = "additional";
    private static final String PARAMETER_ATIME_FROM = "atime_from";
    private static final String PARAMETER_ATIME_TO = "atime_to";
    private static final String PARAMETER_CRTIME_FROM = "crtime_from";
    private static final String PARAMETER_CRTIME_TO = "crtime_to";
    private static final String PARAMETER_EXTENSION = "extension";
    private static final String PARAMETER_FILETYPE = "filetype";
    private static final String PARAMETER_FOLDER_PATH = "folder_path";
    private static final String PARAMETER_GROUP = "group";
    private static final String PARAMETER_LIMIT = "limit";
    private static final String PARAMETER_MTIME_FROM = "mtime_from";
    private static final String PARAMETER_MTIME_TO = "mtime_to";
    private static final String PARAMETER_OFFSET = "offset";
    private static final String PARAMETER_OWNER = "owner";
    private static final String PARAMETER_PATTERN = "pattern";
    private static final String PARAMETER_RECURSIVE = "recursive";
    private static final String PARAMETER_SIZE_FROM = "size_from";
    private static final String PARAMETER_SIZE_TO = "size_to";
    private static final String PARAMETER_SORT_DIRECTION = "sort_direction";
    private static final String PARAMETER_SORT_BY = "sort_by";
    private static final String PARAMETER_TASKID = "taskid";

    // Parameters values
    private static final String PARAMETER_VALUE_ADDITIONAL = "real_path,size,owner,time,perm,type";

    @Autowired
    private TimeZoneUtil timeZoneUtil;

    @Value("${dsm.webapi.synchronousSearch.timeout:30}")
    private int synchronousSearchTimeout;

    private Function<LocalDateTime, String> localDateToTimeStringFunction;

    public SearchServiceImpl() {
        super(API_ID);
    }

    @PostConstruct
    public void initSearchService() {
        localDateToTimeStringFunction = localDateTime -> Long.toString(localDateTime.toEpochSecond(timeZoneUtil.getZoneOffset()));
    }

    @Override
    public String start(String searchedFolderPath, boolean recursive, SearchCriteria searchCriteria) {
        DsmWebapiRequest request = new DsmWebapiRequest(getApiId(), API_VERSION, getApiInfo().getPath(), METHOD_START)
                .parameter(PARAMETER_FOLDER_PATH, searchedFolderPath)
                .parameter(PARAMETER_RECURSIVE, Boolean.toString(recursive))
                .optionalStringParameter(PARAMETER_PATTERN, Joiner.on(',').join(searchCriteria.getPatterns()))
                .optionalStringParameter(PARAMETER_EXTENSION, Joiner.on(',').join(searchCriteria.getExtensions()))
                .optionalParameter(PARAMETER_FILETYPE, searchCriteria.getFileType(), FileType::getRepresentation)
                .optionalParameter(PARAMETER_SIZE_FROM, searchCriteria.getSizeFrom())
                .optionalParameter(PARAMETER_SIZE_TO, searchCriteria.getSizeTo())
                .optionalParameter(PARAMETER_MTIME_FROM, searchCriteria.getLastModifiedTimeFrom(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_MTIME_TO, searchCriteria.getLastModifiedTimeTo(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_CRTIME_FROM, searchCriteria.getCreationTimeFrom(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_CRTIME_TO, searchCriteria.getCreationTimeTo(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_ATIME_FROM, searchCriteria.getLastAccessTimeFrom(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_ATIME_TO, searchCriteria.getLastAccessTimeTo(), localDateToTimeStringFunction)
                .optionalParameter(PARAMETER_OWNER, searchCriteria.getOwnerUserName())
                .optionalParameter(PARAMETER_GROUP, searchCriteria.getGroupName());
        StartSearchResponse call = getDsmWebapiClient().call(request, StartSearchResponse.class);
        return call.getData().getTaskId();
    }

    @Override
    public boolean isFinished(String taskId) {
        return getResult(taskId, new PaginationAndSorting(0, 1, PaginationAndSorting.Sort.NAME, PaginationAndSorting.SortDirection.ASC)).isFinished();
    }

    @Override
    public SearchResult getResult(String taskId, PaginationAndSorting paginationAndSorting) {
        DsmWebapiRequest request = new DsmWebapiRequest(getApiId(), "1", getApiInfo().getPath(), "list")
                .parameter(PARAMETER_TASKID, taskId)
                .parameter(PARAMETER_OFFSET, paginationAndSorting.getOffset())
                .parameter(PARAMETER_LIMIT, paginationAndSorting.getLimit())
                .parameter(PARAMETER_SORT_BY, paginationAndSorting.getSortBy().getRepresentation())
                .parameter(PARAMETER_SORT_DIRECTION, paginationAndSorting.getSortDirection().getRepresentation())
                .parameter(PARAMETER_ADDITIONAL, PARAMETER_VALUE_ADDITIONAL)
                ;
        SearchResultResponse call = getDsmWebapiClient().call(request, SearchResultResponse.class);
        return call.getData();
    }

    @Override
    public List<File> getResult(String taskId) {
        return getResult(taskId, PaginationAndSorting.DEFAULT_PAGINATION_AND_SORTING).getElements();
    }

    @Override
    public void stop(List<String> taskIds) {
        DsmWebapiRequest request = new DsmWebapiRequest(getApiId(), API_VERSION, getApiInfo().getPath(), METHOD_STOP)
                .parameter(PARAMETER_TASKID, Joiner.on(',').join(taskIds));
        DsmWebapiResponse response = getDsmWebapiClient().call(request, DsmWebapiResponse.class);
        // TODO handle failure
    }

    @Override
    public void stop(String taskId) {
        stop(Collections.singletonList(taskId));
    }

    @Override
    public void clean(List<String> taskIds) {
        DsmWebapiRequest request = new DsmWebapiRequest(getApiId(),  API_VERSION, getApiInfo().getPath(), METHOD_CLEAN)
                .parameter(PARAMETER_TASKID, Joiner.on(',').join(taskIds));
        DsmWebapiResponse response = getDsmWebapiClient().call(request, DsmWebapiResponse.class);
        // TODO handle failure
    }

    @Override
    public void clean(String taskId) {
        clean(Collections.singletonList(taskId));
    }

    @Override
    public List<File> synchronousSearch(String searchedFolderPath, boolean recursive, SearchCriteria searchCriteria) {
        String taskId = start(searchedFolderPath, recursive, searchCriteria);
        try {
            await().atMost(synchronousSearchTimeout, TimeUnit.SECONDS).until(() -> {
                return SearchServiceImpl.this.isFinished(taskId);
            });
            return getResult(taskId);
        } catch (ConditionTimeoutException e) {
            stop(taskId);
            throw new TaskTimeOutException(e);
        } finally {
            clean(taskId);
        }
    }

    public static class StartSearchResponse extends DsmWebapiResponse<TaskId> {

        public StartSearchResponse(@JsonProperty("success") boolean success, @JsonProperty("data") TaskId data, @JsonProperty("error") DsmWebApiResponseError error) {