Commit 8c2bd669 authored by Szabolcs Gyurko's avatar Szabolcs Gyurko
Browse files

Added UI endpoint to retrieve full/page node list

parent f87908ff
Pipeline #301 failed with stage
in 16 seconds
package com.jeff_cms.jeff.node.model;
package com.jeff_cms.jeff.model;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
......
......@@ -26,7 +26,7 @@ package com.jeff_cms.jeff.node.controller;
import com.jeff_cms.jeff.model.Node;
import com.jeff_cms.jeff.node.exception.UnauthorisedException;
import com.jeff_cms.jeff.node.model.BatchReadRequest;
import com.jeff_cms.jeff.model.BatchReadRequest;
import com.jeff_cms.jeff.node.service.ContentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
......
......@@ -28,7 +28,7 @@ import com.jeff_cms.jeff.model.Node;
import com.jeff_cms.jeff.node.Constants;
import com.jeff_cms.jeff.node.exception.*;
import com.jeff_cms.jeff.node.jcr.JcrSessionFactory;
import com.jeff_cms.jeff.node.model.BatchReadRequest;
import com.jeff_cms.jeff.model.BatchReadRequest;
import com.jeff_cms.jeff.node.utils.JcrUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.jackrabbit.value.DateValue;
......
......@@ -33,4 +33,3 @@ mq:
queue: jeffQueue
exchange: jeffExchange
binding-key: JEFF.EVENT.NODE
......@@ -32,7 +32,7 @@ spring:
---
spring:
profiles: redis-cache
profiles: redis
autoconfigure:
exclude:
......@@ -25,7 +25,7 @@
spring:
profiles:
active: default,amq,oauth2,author,jcr,cache
active: default,amq,oauth2,author,jcr,cache,no-redis
mvc:
favicon:
......@@ -34,12 +34,6 @@ spring:
main:
banner-mode: 'off'
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
http:
multipart:
max-file-size: 104857600
......@@ -70,3 +64,20 @@ executor:
core-pool-size: 8
max-pool-size: 48
queue-capacity: 250
---
spring:
profiles: no-redis
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
---
spring:
profiles: no-amq
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
......@@ -76,7 +76,7 @@ public class OAuth2Config extends GlobalMethodSecurityConfiguration {
}
@Bean
OAuth2RestOperations restTemplate(@Qualifier("oauthClientResourceDetails") OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
OAuth2RestOperations oAuth2RestOperations(@Qualifier("oauthClientResourceDetails") OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
final AccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, new DefaultOAuth2ClientContext(accessTokenRequest));
......
package com.jeff_cms.jeff.web.controller;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.jeff_cms.jeff.web.model.NodeTree;
import com.jeff_cms.jeff.web.service.UIService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Controller with endpoints for the admin UI.
*/
@RestController
@RequestMapping("/ui")
@CrossOrigin("*")
@Slf4j
public class UIController {
private final UIService uiService;
public UIController(final UIService uiService) {
this.uiService = uiService;
}
@GetMapping("/pagetree")
public NodeTree getPageNodeTree() {
return uiService.getPageNodeTree();
}
@GetMapping("/nodetree")
public NodeTree getNodeTree() {
return uiService.getFullNodeTree();
}
}
package com.jeff_cms.jeff.web.converter;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.jeff_cms.jeff.model.Node;
import com.jeff_cms.jeff.web.model.NodeTree;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Converts a list of Nodes to a NodeTree model.
*/
@Component
public class NodeListToNodeTreeConverter implements Converter<List<Node>, NodeTree> {
private static final Pattern NODE_NAME = Pattern.compile(".*/([^/]+)$");
@Override
public NodeTree convert(final List<Node> nodes) {
final NodeTree nodeTree = new NodeTree();
nodeTree.setName("ROOT");
nodeTree.setPath("/");
if (nodes.isEmpty())
return nodeTree;
final String path = nodes.get(0).getPath();
nodeTree.setPath(path);
nodeTree.setChildren(getChildren(nodes, "/".equals(path) ? "" : path));
return nodeTree;
}
private List<NodeTree> getChildren(final List<Node> nodes, final String path) {
final Pattern sameLevelNodes = Pattern.compile(path + "/[^/]+");
return nodes.stream()
.filter(node -> sameLevelNodes.matcher(node.getPath()).matches())
.map(node -> {
final NodeTree nodeTree = new NodeTree();
nodeTree.setName(NODE_NAME.matcher(node.getPath()).replaceAll("$1"));
nodeTree.setPath(node.getPath());
nodeTree.setChildren(getChildren(nodes, node.getPath()));
return nodeTree;
})
.collect(Collectors.toList());
}
}
package com.jeff_cms.jeff.web.model;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.List;
/**
* Model for a hierarchical Node Representation.
*/
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class NodeTree {
private String name;
private String path;
private List<NodeTree> children;
}
......@@ -61,7 +61,7 @@ public class PageService {
final ResponseEntity<Node> responseEntity = restTemplate.exchange(
urlUtils.getPageContentUrl(path),
HttpMethod.GET,
authenticatedEntity(null),
urlUtils.authenticatedEntity(null),
Node.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
......@@ -83,7 +83,7 @@ public class PageService {
return restTemplate.exchange(
urlUtils.getContentUrl(),
HttpMethod.POST,
authenticatedEntity(conversionService.convert(page, Node.class)),
urlUtils.authenticatedEntity(conversionService.convert(page, Node.class)),
String.class).getStatusCode();
}
......@@ -99,7 +99,7 @@ public class PageService {
return restTemplate.exchange(
urlUtils.getContentUrl(),
HttpMethod.PUT,
authenticatedEntity(conversionService.convert(page, Node.class)),
urlUtils.authenticatedEntity(conversionService.convert(page, Node.class)),
String.class
).getStatusCode();
}
......@@ -113,15 +113,8 @@ public class PageService {
return restTemplate.exchange(
urlUtils.getPageContentUrl(path),
HttpMethod.DELETE,
authenticatedEntity(null),
urlUtils.authenticatedEntity(null),
String.class
).getStatusCode();
}
private <T> HttpEntity<T> authenticatedEntity(T body) {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + urlUtils.getClientAccessToken());
return new HttpEntity<T>(body, httpHeaders);
}
}
package com.jeff_cms.jeff.web.service;
/*
Copyright (C) 2018 Szabolcs Gyurko <szabolcs@hacking.hu>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import com.jeff_cms.jeff.model.BatchReadRequest;
import com.jeff_cms.jeff.model.Node;
import com.jeff_cms.jeff.web.model.NodeTree;
import com.jeff_cms.jeff.web.util.UrlUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Service exposing misc functions for admin UI.
*/
@Service
@Slf4j
@CacheConfig(cacheNames = "ui")
public class UIService {
private final ConversionService conversionService;
private final RestTemplate restTemplate;
private final UrlUtils urlUtils;
public UIService(final RestTemplate restTemplate, final UrlUtils urlUtils, final ConversionService conversionService) {
this.restTemplate = restTemplate;
this.urlUtils = urlUtils;
this.conversionService = conversionService;
}
/**
* Gets a page node list representation for the UI.
* @return NodeTree object
*/
@Cacheable
public NodeTree getPageNodeTree() {
return getNodeTree(urlUtils.getPageContentPath(""));
}
/**
* Gets a component node list representation for the UI.
* @return NodeTree object
*/
@Cacheable
public NodeTree getFullNodeTree() {
return getNodeTree("/");
}
private NodeTree getNodeTree(final String path) {
final List<Node> allNodes = new ArrayList<>();
List<Node> children = getNodeTreeBatch(Collections.singletonList(path));
while (!children.isEmpty()) {
allNodes.addAll(children);
children = getNodeTreeBatch(children.stream().flatMap(n -> n.getChildren().stream()
.map(c -> n.getPath().endsWith("/") ? n.getPath() + c : n.getPath() + "/" + c))
.collect(Collectors.toList()));
}
return (NodeTree) conversionService.convert(
allNodes,
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Node.class)),
TypeDescriptor.valueOf(NodeTree.class)
);
}
private List<Node> getNodeTreeBatch(final List<String> paths) {
if (paths.isEmpty())
return Collections.emptyList();
final BatchReadRequest batchReadRequest = new BatchReadRequest();
batchReadRequest.setPreFetchBinary(false);
batchReadRequest.setNodes(paths);
final ResponseEntity<List<Node>> result = restTemplate.exchange(
urlUtils.getBatchUrl(),
HttpMethod.POST,
urlUtils.authenticatedEntity(batchReadRequest),
new ParameterizedTypeReference<List<Node>>() {
});
return result.getBody();
}
}
......@@ -25,6 +25,8 @@ package com.jeff_cms.jeff.web.util;
*/
import com.jeff_cms.jeff.web.config.JeffConfiguration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
......@@ -67,12 +69,30 @@ public class UrlUtils {
return String.format("%s%s", jeffConfiguration.getPagesNodePath(), pagePath);
}
/**
* Returns the access token from the request context.
* @return Access Token
*/
public String getClientAccessToken() {
private String getClientAccessToken() {
return RequestContextHolder.getRequestAttributes()
.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, RequestAttributes.SCOPE_REQUEST).toString();
}
/**
* Returns the jeff-node batch service URL.
* @return the jeff-node batch service URL.
*/
public String getBatchUrl() {
return String.format("%s/batch/read", jeffConfiguration.getApiNode());
}
/**
* Creates an HTTP entity and adds a bearer token from the current request context.
* @param body Body object
* @param <T> Type of the body
* @return A new HTTP Entity object
*/
public <T> HttpEntity<T> authenticatedEntity(T body) {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Authorization", "Bearer " + getClientAccessToken());
return new HttpEntity<T>(body, httpHeaders);
}
}
......@@ -32,7 +32,8 @@ spring:
---
spring:
profiles: redis-cache
profiles: redis
autoconfigure:
exclude:
......@@ -25,7 +25,7 @@
spring:
profiles:
active: default,oauth2,cache
active: default,oauth2,cache,no-redis
mvc:
favicon:
......@@ -34,11 +34,6 @@ spring:
main:
banner-mode: 'off'
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
logging:
level:
com.jeff_cms: DEBUG
......@@ -64,7 +59,6 @@ executor:
queue-capacity: 250
---
spring:
profiles: prod
......@@ -77,3 +71,12 @@ server:
error:
whitelabel:
enabled: false
---
spring:
profiles: no-redis
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
......@@ -15,4 +15,14 @@
transactionalMode="off">
</cache>
<cache name="ui"
maxElementsInMemory="1000"
eternal="false"
overflowToDisk="false"
timeToLiveSeconds="60"
timeToIdleSeconds="0"
memoryStoreEvictionPolicy="LFU"
transactionalMode="off">
</cache>
</ehcache>
\ No newline at end of file
......@@ -69,7 +69,7 @@ public class PageServiceTest {
@Before
public void setupUrlUtils() {
when(urlUtils.getClientAccessToken()).thenReturn("1c9ff729-b693-48ca-93e2-c8b2c74cfe18");
when(urlUtils.authenticatedEntity(any())).thenReturn(new HttpEntity<>(null));
when(urlUtils.getPageContentUrl(anyString())).thenReturn("http://localhost:443/content/test");
when(urlUtils.getContentUrl()).thenReturn("http://localhost:443/content/");
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment