From 004a5ea8b115cddcfd2cf642ad3932fbd377d6fc Mon Sep 17 00:00:00 2001 From: Osmar Benavidez Date: Tue, 20 Feb 2024 06:38:46 -0600 Subject: [PATCH] fix v2 issue #9 --- .../controller/admin/AdminController.java | 332 +++++++++--------- .../repository/AnalysisRepository.java | 219 ++++++------ 2 files changed, 291 insertions(+), 260 deletions(-) diff --git a/datanode/src/main/java/com/odysseusinc/arachne/datanode/controller/admin/AdminController.java b/datanode/src/main/java/com/odysseusinc/arachne/datanode/controller/admin/AdminController.java index d106f779..64414234 100644 --- a/datanode/src/main/java/com/odysseusinc/arachne/datanode/controller/admin/AdminController.java +++ b/datanode/src/main/java/com/odysseusinc/arachne/datanode/controller/admin/AdminController.java @@ -58,190 +58,208 @@ @RestController public class AdminController extends BaseController { - public static final int SUGGEST_LIMIT = 10; - public static final int DEFAULT_PAGE_SIZE = 10; - private final Map>> propertiesMap = new HashMap<>(); - @Autowired - private AnalysisToSubmissionDTOConverter analysisToSubmissionDTO; - @Autowired - private GenericConversionService conversionService; - @Autowired - private AnalysisRepository analysisRepository; - @Autowired - private AnalysisService analysisService; - @Autowired - private DataNodeService dataNodeService; - @Autowired - private ExecutionEngineIntegrationService executionEngineIntegrationService; - - public AdminController(UserService userService) { - super(userService); - initProps(); - } + public static final int SUGGEST_LIMIT = 10; + public static final int DEFAULT_PAGE_SIZE = 10; + private final Map>> propertiesMap = new HashMap<>(); + @Autowired + private AnalysisToSubmissionDTOConverter analysisToSubmissionDTO; + @Autowired + private GenericConversionService conversionService; + @Autowired + private AnalysisRepository analysisRepository; + @Autowired + private AnalysisService analysisService; + @Autowired + private DataNodeService dataNodeService; + @Autowired + private ExecutionEngineIntegrationService executionEngineIntegrationService; + + public AdminController(UserService userService) { + super(userService); + initProps(); + } + + @ApiOperation(value = "Get all admins", hidden = true) + @GetMapping("/api/v1/admin/admins") + public JsonResult> getAdmins( + @RequestParam(name = "sortBy", required = false) String sortBy, + @RequestParam(name = "sortAsc", required = false) Boolean sortAsc + ) throws PermissionDeniedException { + + JsonResult> result; + List users = userService.getAllAdmins(sortBy, sortAsc); + List dtos = users.stream() + .map(user -> conversionService.convert(user, UserDTO.class)) + .collect(Collectors.toList()); + result = new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); + result.setResult(dtos); + return result; + } + + @ApiOperation("Suggests user according to query to add admin") + @GetMapping("/api/v1/admin/admins/suggest") + public JsonResult> suggestAdmins( + Principal principal, + @RequestParam("query") String query, + @RequestParam(value = "limit", required = false) Integer limit + ) { + + JsonResult> result = new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); + userService + .findByUsername(principal.getName()) + .ifPresent(user -> { + List users = userService.suggestNotAdmin(user, query, + limit == null ? SUGGEST_LIMIT : limit); + result.setResult(users.stream().map(u -> conversionService + .convert(u, UserDTO.class)) + .collect(Collectors.toList()) + ); + }); + return result; + } - @ApiOperation(value = "Get all admins", hidden = true) - @GetMapping("/api/v1/admin/admins") - public JsonResult> getAdmins( - @RequestParam(name = "sortBy", required = false) String sortBy, - @RequestParam(name = "sortAsc", required = false) Boolean sortAsc - ) throws PermissionDeniedException { - - JsonResult> result; - List users = userService.getAllAdmins(sortBy, sortAsc); - List dtos = users.stream() - .map(user -> conversionService.convert(user, UserDTO.class)) - .collect(Collectors.toList()); - result = new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); - result.setResult(dtos); - return result; - } + @ApiOperation("Remove admin") + @DeleteMapping("/api/v1/admin/admins/{username:.+}") + public JsonResult removeAdmin(@PathVariable String username) { - @ApiOperation("Suggests user according to query to add admin") - @GetMapping("/api/v1/admin/admins/suggest") - public JsonResult> suggestAdmins( - Principal principal, - @RequestParam("query") String query, - @RequestParam(value = "limit", required = false) Integer limit - ) { - - JsonResult> result = new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); - userService - .findByUsername(principal.getName()) - .ifPresent(user -> { - List users = userService.suggestNotAdmin(user, query, limit == null ? SUGGEST_LIMIT : limit); - result.setResult(users.stream().map(u -> conversionService - .convert(u, UserDTO.class)) - .collect(Collectors.toList()) - ); - }); - return result; - } + userService.findByUsername(username).ifPresent(user -> userService.remove(user.getId())); + return new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); + } - @ApiOperation("Remove admin") - @DeleteMapping("/api/v1/admin/admins/{username:.+}") - public JsonResult removeAdmin(@PathVariable String username) { + @ApiOperation(value = "Invalidate all unfinished analyses") + @PostMapping(Constants.Api.Analysis.INVALIDATE_ALL_UNFINISHED) + public Integer invalidateAllUnfinishedAnalyses(final Principal principal) + throws PermissionDeniedException { - userService.findByUsername(username).ifPresent(user -> userService.remove(user.getId())); - return new JsonResult<>(JsonResult.ErrorCode.NO_ERROR); + if (principal == null) { + throw new AuthException("user not found"); } - @ApiOperation(value = "Invalidate all unfinished analyses") - @PostMapping(Constants.Api.Analysis.INVALIDATE_ALL_UNFINISHED) - public Integer invalidateAllUnfinishedAnalyses(final Principal principal) throws PermissionDeniedException { - - if (principal == null) { - throw new AuthException("user not found"); + return analysisService.invalidateAllUnfinishedAnalyses(getUser(principal)); + } + + @ApiOperation(value = "list submissions") + @GetMapping("/api/v1/admin/submissions") + public Page list(@PageableDefault(value = DEFAULT_PAGE_SIZE, sort = "id", + direction = Sort.Direction.DESC) Pageable pageable) { + + Page analyses; + String sortField = isFinishedSort(pageable) ? "finished" + : isSubmittedSort(pageable) ? "submitted" : isStudySort(pageable) ? "study" + : (isAnalysisSort(pageable) ? "analysis" : (isStatusSort(pageable) ? "status" : "")); + + if (pageable.getSort() == null || "".equals(sortField)) { + Pageable p; + if (!isCustomSort(pageable)) { + p = pageable; + } else { + p = buildPageRequest(pageable); + } + analyses = analysisRepository.findAll(p); + } else { + if (sortField.contains("finished") || sortField.contains("submitted")) { + analyses = analysisRepository.findAllPagedOrderBySubmittedAndFinished( + pageable.getSort().get().findFirst().get().getDirection().name() + , sortField + , PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); + } else { + analyses = analysisRepository.findAllPagedOrderByAnalysisStudyStatus( + pageable.getSort().get().findFirst().get().getDirection().name() + , sortField + , PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); } - - return analysisService.invalidateAllUnfinishedAnalyses(getUser(principal)); } + return analyses.map(analysis -> analysisToSubmissionDTO.convert(analysis)); + } - @ApiOperation(value = "list submissions") - @GetMapping("/api/v1/admin/submissions") - public Page list(@PageableDefault(value = DEFAULT_PAGE_SIZE, sort = "id", - direction = Sort.Direction.DESC) Pageable pageable) { - Page analyses; - String sortField= isFinishedSort(pageable) ?"finished": - (isSubmittedSort(pageable) ?"submitted": - (isStatusSort(pageable)?"status":"")); - if(pageable.getSort() == null || "".equals(sortField)) - { - Pageable p; - if (!isCustomSort(pageable)) { - p = pageable; - } else { - p = buildPageRequest(pageable); - } - analyses = analysisRepository.findAll(p); - }else{ - analyses = analysisRepository.findAllPagedOrderByCustomFields( pageable.getSort().get().findFirst().get().getDirection().name() - ,sortField - ,PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); - } - return analyses.map(analysis -> analysisToSubmissionDTO.convert(analysis)); - } + @ApiOperation(value = "get execution engine status") + @GetMapping("/api/v1/admin/execution-engine/status") + public EngineStatusResponse getExecutionEngineStatus() { + return new EngineStatusResponse(executionEngineIntegrationService.getExecutionEngineStatus()); + } + + protected Pageable buildPageRequest(Pageable pageable) { - @ApiOperation(value = "get execution engine status") - @GetMapping("/api/v1/admin/execution-engine/status") - public EngineStatusResponse getExecutionEngineStatus() { - return new EngineStatusResponse(executionEngineIntegrationService.getExecutionEngineStatus()); + if (pageable.getSort() == null) { + return pageable; + } + PageRequest result; + List properties = new LinkedList<>(); + Sort.Direction direction = Sort.Direction.ASC; + for (Sort.Order order : pageable.getSort()) { + direction = order.getDirection(); + String property = order.getProperty(); + if (propertiesMap.containsKey(property)) { + propertiesMap.get(property).accept(properties); + } else { + properties.add(property); + } } + result = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), direction, + properties.toArray(new String[properties.size()])); - protected Pageable buildPageRequest(Pageable pageable) { + return result; + } - if (pageable.getSort() == null) { - return pageable; - } - PageRequest result; - List properties = new LinkedList<>(); - Sort.Direction direction = Sort.Direction.ASC; - for (Sort.Order order : pageable.getSort()) { - direction = order.getDirection(); - String property = order.getProperty(); - if (propertiesMap.containsKey(property)) { - propertiesMap.get(property).accept(properties); - } else { - properties.add(property); - } - } - result = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), direction, - properties.toArray(new String[properties.size()])); + private boolean isCustomSort(final Pageable pageable) { - return result; - } + return isSortOf(pageable, propertiesMap::containsKey); + } - private boolean isCustomSort(final Pageable pageable) { + private boolean isStatusSort(final Pageable pageable) { - return isSortOf(pageable, propertiesMap::containsKey); - } + return isSortOf(pageable, "status"::equals); + } - private boolean isStatusSort(final Pageable pageable) { + private boolean isAnalysisSort(final Pageable pageable) { + return isSortOf(pageable, "analysis"::equals); + } - return isSortOf(pageable, "status"::equals); - } + private boolean isStudySort(final Pageable pageable) { + return isSortOf(pageable, "study"::equals); + } - private boolean isSubmittedSort(final Pageable pageable) { + private boolean isSubmittedSort(final Pageable pageable) { - return isSortOf(pageable, "submitted"::equals); - } + return isSortOf(pageable, "submitted"::equals); + } - private boolean isFinishedSort(final Pageable pageable) { + private boolean isFinishedSort(final Pageable pageable) { - return isSortOf(pageable, "finished"::equals); - } + return isSortOf(pageable, "finished"::equals); + } - private boolean isSortOf(final Pageable pageable, Function predicate) { + private boolean isSortOf(final Pageable pageable, Function predicate) { - if (pageable.getSort() == null) { - return false; - } - for (Sort.Order order : pageable.getSort()) { - if (predicate.apply(order.getProperty())) { - return true; - } - } - return false; + if (pageable.getSort() == null) { + return false; + } + for (Sort.Order order : pageable.getSort()) { + if (predicate.apply(order.getProperty())) { + return true; + } } + return false; + } - protected void initProps() { + protected void initProps() { - propertiesMap.put("author.fullName", p -> { - p.add("author.firstName"); - p.add("author.lastName"); - }); - propertiesMap.put("fullName", p -> { - p.add("author.firstName"); - p.add("author.lastName"); - }); - propertiesMap.put("analysis", p -> p.add("title")); - propertiesMap.put("study", p -> p.add("studyTitle")); - propertiesMap.put("status", p -> p.add("journal.state")); - } + propertiesMap.put("author.fullName", p -> { + p.add("author.firstName"); + p.add("author.lastName"); + }); + propertiesMap.put("fullName", p -> { + p.add("author.firstName"); + p.add("author.lastName"); + }); + } - private class EngineStatusResponse { - public EngineStatusResponse(final ExecutionEngineStatus status) { - this.status = status; - } - public ExecutionEngineStatus status; + private class EngineStatusResponse { + + public EngineStatusResponse(final ExecutionEngineStatus status) { + this.status = status; } + + public ExecutionEngineStatus status; + } } diff --git a/datanode/src/main/java/com/odysseusinc/arachne/datanode/repository/AnalysisRepository.java b/datanode/src/main/java/com/odysseusinc/arachne/datanode/repository/AnalysisRepository.java index ad572437..15aa16f9 100644 --- a/datanode/src/main/java/com/odysseusinc/arachne/datanode/repository/AnalysisRepository.java +++ b/datanode/src/main/java/com/odysseusinc/arachne/datanode/repository/AnalysisRepository.java @@ -28,111 +28,124 @@ public interface AnalysisRepository extends JpaRepository { - Optional findById(Long id); + Optional findById(Long id); - @Query(nativeQuery = true, value = - "SELECT analyses.* FROM analyses WHERE analyses.id = :id AND analyses.callback_password = :password") - Optional findOneExecuting(@Param("id") Long id, @Param("password") String password); + @Query(nativeQuery = true, value = + "SELECT analyses.* FROM analyses WHERE analyses.id = :id AND analyses.callback_password = :password") + Optional findOneExecuting(@Param("id") Long id, @Param("password") String password); - @Query(nativeQuery = true, value = - "SELECT analyses.* " - + "FROM analyses " - + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " - + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " - + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " - + " WHERE journal.state = :state AND analyses.central_id IS NOT NULL") - List findAllByState(@Param("state") String state); + @Query(nativeQuery = true, value = + "SELECT analyses.* " + + "FROM analyses " + + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " + + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " + + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " + + " WHERE journal.state = :state AND analyses.central_id IS NOT NULL") + List findAllByState(@Param("state") String state); - @Query(nativeQuery = true, value = - "SELECT analyses.* " - + "FROM analyses " - + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " - + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " - + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " - + " WHERE journal.state NOT IN (:states)") - List findAllByNotStateIn(@Param("states") List states); + @Query(nativeQuery = true, value = + "SELECT analyses.* " + + "FROM analyses " + + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " + + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " + + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " + + " WHERE journal.state NOT IN (:states)") + List findAllByNotStateIn(@Param("states") List states); - @Query(nativeQuery = true, value = - "SELECT analyses.* " - + " FROM analyses " - + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " - + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " - + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " - + " WHERE journal.state = :state AND journal.date < :time") - List findAllExecutingMoreThan(@Param("state") String state, @Param("time") Date time); - @Query(nativeQuery = true, value = - "select a.*\n" + - " ,JS.submitted \n" + - " ,JS.finished\n" + - " ,JS.status\n" + - " ,SV.id\n" + - "from analyses a \n" + - " JOIN (\n" + - " SELECT distinct analysis_id, \n" + - " min(date) over(partition by j.analysis_id) AS submitted, \n" + - " max(\n" + - " case j.state \n" + - " when 'EXECUTING' then null \n" + - " when 'CREATED' then null\n" + - " else j.date \n" + - " end \n" + - " ) over(partition by j.analysis_id) as finished,\n" + - " FIRST_VALUE(j.state) over(partition by j.analysis_id order by j.date desc) status\n" + - " FROM analysis_state_journal j\n" + - " ) AS JS ON a.id=JS.analysis_id\n" + - " LEFT JOIN (\n" + - " with tab as (\n" + - " values\n" + - " (CAST(1 AS int), 'ABORTED'),\n" + - " (CAST(2 AS int), 'ABORTING'),\n" + - " (CAST(3 AS int), 'CREATED'),\n" + - " (CAST(4 AS int), 'EXECUTED'),\n" + - " (CAST(5 AS int), 'EXECUTING'),\n" + - " (CAST(6 AS int), 'EXECUTION_FAILURE'),\n" + - " (CAST(7 AS int), 'ABORT_FAILURE'),\n" + - " (CAST(8 AS int), NULL),\n" + - " (CAST(9 AS int), 'DEAD')\n" + - " ) \n" + - " select column1 as id,column2 as status\n" + - " from tab\n" + - " )SV ON JS.status=SV.status\n" + - " ORDER BY \n" + - " CASE WHEN :direction = 'ASC' THEN\n" + - " CASE :sortField \n" + - " WHEN 'submitted' THEN CAST(COALESCE(JS.submitted, CAST('epoch' as timestamp)) as text)\n" + - " WHEN 'finished' THEN CAST(COALESCE(JS.finished, CAST('epoch' as timestamp)) as text)\n" + - " WHEN 'status' THEN CAST(SV.id as text)\n" + - " WHEN 'analysis' THEN a.title \n" + - " WHEN 'study' THEN a.study_title \n" + - " ELSE CAST(a.id as text)\n" + - " END \n" + - " END ASC,\n" + - " CASE WHEN :direction = 'DESC' THEN\n" + - " CASE :sortField \n" + - " WHEN 'submitted' THEN CAST(COALESCE(JS.submitted, CAST('epoch' as timestamp)) as text)\n" + - " WHEN 'finished' THEN CAST(COALESCE(JS.finished, CAST('epoch' as timestamp)) as text)\n" + - " WHEN 'status' THEN CAST(SV.id as text)\n" + - " WHEN 'analysis' THEN a.title \n" + - " WHEN 'study' THEN a.study_title \n" + - " ELSE CAST(a.id as text)\n" + - " END \n" + - " END DESC", - countQuery = - "SELECT COUNT(a.*) " + - "FROM analyses a \n" + - " JOIN (\n" + - " SELECT distinct analysis_id, \n" + - " min(date) over(partition by j.analysis_id) AS submitted, \n" + - " max(\n" + - " case j.state \n" + - " when 'EXECUTING' then null \n" + - " when 'CREATED' then null\n" + - " else j.date \n" + - " end \n" + - " ) over(partition by j.analysis_id) as finished,\n" + - " FIRST_VALUE(j.state) over(partition by j.analysis_id order by j.date desc) status\n" + - " FROM analysis_state_journal j\n" + - " ) AS JS ON a.id=JS.analysis_id") - Page findAllPagedOrderByCustomFields(String direction,String sortField,Pageable pageable); + @Query(nativeQuery = true, value = + "SELECT analyses.* " + + " FROM analyses " + + " JOIN analysis_state_journal AS journal ON journal.analysis_id = analyses.id " + + " JOIN (SELECT analysis_id, max(date) AS latest FROM analysis_state_journal " + + " GROUP BY analysis_id) AS FOO ON journal.date = FOO.latest AND journal.analysis_id=FOO.analysis_id " + + " WHERE journal.state = :state AND journal.date < :time") + List findAllExecutingMoreThan(@Param("state") String state, @Param("time") Date time); -} + @Query(nativeQuery = true, + value = + "SELECT a.* \n" + + "FROM analyses a \n" + + " JOIN analysis_state_journal AS J ON J.analysis_id = a.id\n" + + " JOIN (\n" + + " SELECT J.analysis_id, \n" + + " MAX(J.date) AS finished\n" + + " FROM analysis_state_journal J\n" + + " GROUP BY J.analysis_id \n" + + " ) JS ON J.analysis_id=JS.analysis_id and J.date = JS.finished \n" + + "ORDER BY CASE WHEN :direction = 'ASC' THEN\n" + + " CASE :sortField \n" + + " WHEN 'status' THEN (CASE WHEN J.state = 'ABORT_FAILURE' THEN 'Failed to Abort' \n" + + + " WHEN J.state = 'EXECUTION_FAILURE' THEN 'Failed'\n" + + " ELSE J.state END )\n" + + " WHEN 'analysis' THEN a.title \n" + + " WHEN 'study' THEN a.study_title \n" + + " END \n" + + " END ASC,\n" + + " CASE WHEN :direction = 'DESC' THEN\n" + + " CASE :sortField \n" + + " WHEN 'status' THEN (CASE WHEN J.state = 'ABORT_FAILURE' THEN 'Failed to Abort' \n" + + + " WHEN J.state = 'EXECUTION_FAILURE' THEN 'Failed'\n" + + " ELSE J.state END ) \n" + + " WHEN 'analysis' THEN a.title \n" + + " WHEN 'study' THEN a.study_title \n" + + " END \n" + + " END DESC ", + countQuery = + "SELECT a.* \n" + + "FROM analyses a \n" + + " JOIN analysis_state_journal AS J ON J.analysis_id = a.id\n" + + " JOIN (\n" + + " SELECT J.analysis_id, \n" + + " MAX(J.date) AS finished\n" + + " FROM analysis_state_journal J\n" + + " GROUP BY J.analysis_id \n" + + " ) JS ON J.analysis_id=JS.analysis_id and J.date = JS.finished") + Page findAllPagedOrderByAnalysisStudyStatus(String direction, String sortField, + Pageable pageable); + + @Query(nativeQuery = true, value = + "SELECT a.* \n" + + "FROM analyses a \n" + + " JOIN analysis_state_journal AS J ON J.analysis_id = a.id\n" + + " JOIN (\n" + + " SELECT J.analysis_id, \n" + + " MAX(J.date) AS finished, \n" + + " MIN(J.date) AS submitted \n" + + " FROM analysis_state_journal J\n" + + " GROUP BY J.analysis_id \n" + + " ) JS ON J.analysis_id=JS.analysis_id AND J.date = JS.finished \n" + + "ORDER BY \n" + + " CASE WHEN :direction = 'ASC' THEN\n" + + " CASE :sortField \n" + + " WHEN 'submitted' THEN JS.submitted \n" + + " WHEN 'finished' THEN CASE WHEN J.state IN('EXECUTING','CREATED') THEN null \n" + + + " ELSE JS.finished \n" + + " END \n" + + " END \n" + + " END ASC NULLS LAST,\n" + + " CASE WHEN :direction = 'DESC' THEN\n" + + " CASE :sortField \n" + + " WHEN 'submitted' THEN JS.submitted \n" + + " WHEN 'finished' THEN CASE WHEN J.state IN('EXECUTING','CREATED') THEN null\n" + + + " ELSE JS.finished \n" + + " END \n" + + " END \n" + + " END DESC NULLS LAST", + countQuery = + "SELECT COUNT(a.*) " + + "FROM analyses a \n" + + " JOIN analysis_state_journal AS J ON J.analysis_id = a.id\n" + + " JOIN (\n" + + " SELECT J.analysis_id, \n" + + " MAX(J.date) AS finished, \n" + + " MIN(J.date) AS submitted \n" + + " FROM analysis_state_journal J \n" + + " GROUP BY J.analysis_id \n" + + " ) JS ON J.analysis_id=JS.analysis_id AND J.date = JS.finished ") + Page findAllPagedOrderBySubmittedAndFinished(String direction, String sortField, + Pageable pageable); +} \ No newline at end of file