diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/PluginConfig.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/PluginConfig.java index 17da5ccc..dcfa65ef 100644 --- a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/PluginConfig.java +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/PluginConfig.java @@ -40,7 +40,10 @@ import org.sourcelab.kafka.webview.ui.manager.plugin.PluginFactory; import org.sourcelab.kafka.webview.ui.manager.plugin.UploadManager; import org.sourcelab.kafka.webview.ui.manager.sasl.SaslUtility; +import org.sourcelab.kafka.webview.ui.manager.ui.recentasset.RecentAssetManager; import org.sourcelab.kafka.webview.ui.plugin.filter.RecordFilter; +import org.sourcelab.kafka.webview.ui.repository.ClusterRepository; +import org.sourcelab.kafka.webview.ui.repository.ViewRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; @@ -213,4 +216,9 @@ public SaslUtility getSaslUtility(final SecretManager secretManager) { public SensitiveConfigScrubber getSensitiveConfigScrubber(final SaslUtility saslUtility) { return new SensitiveConfigScrubber(saslUtility); } + + @Bean + public RecentAssetManager recentAssetManager(final ClusterRepository clusterRepository, final ViewRepository viewRepository) { + return new RecentAssetManager(clusterRepository, viewRepository); + } } diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/BaseController.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/BaseController.java index d087f2f7..7ce25e42 100644 --- a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/BaseController.java +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/BaseController.java @@ -25,6 +25,10 @@ package org.sourcelab.kafka.webview.ui.controller; import org.sourcelab.kafka.webview.ui.configuration.AppProperties; +import org.sourcelab.kafka.webview.ui.manager.ui.recentasset.RecentAsset; +import org.sourcelab.kafka.webview.ui.manager.ui.recentasset.RecentAssetManager; +import org.sourcelab.kafka.webview.ui.manager.ui.recentasset.RecentAssetStorage; +import org.sourcelab.kafka.webview.ui.manager.ui.recentasset.RecentAssetType; import org.sourcelab.kafka.webview.ui.manager.user.CustomUserDetails; import org.sourcelab.kafka.webview.ui.model.Cluster; import org.sourcelab.kafka.webview.ui.model.View; @@ -39,7 +43,10 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; +import java.util.List; /** * Base Controller w/ common code. @@ -55,6 +62,9 @@ public abstract class BaseController { @Autowired private AppProperties appProperties; + @Autowired + private RecentAssetManager recentAssetManager; + /** * Determine if the current user is logged in or not. * @return True if so, false if not. @@ -99,15 +109,25 @@ protected String getLoggedInUserSessionId() { * This gets executed for all requests. */ @ModelAttribute - public void addAttributes(Model model) { + public void addAttributes(final Model model, final HttpServletRequest request, final HttpServletResponse response) { // But only if logged in if (!isLoggedIn()) { return; } - // TODO put a limit on these - final Iterable clusters = clusterRepository.findAllByOrderByNameAsc(); - final Iterable views = viewRepository.findAllByOrderByNameAsc(); + // TODO + // If we have less than 11 Clusters + // Just list them ordered by name desc. + // Otherwise show 10 most recent clusters accessed. + // Same for views + + final RecentAssetStorage storage = getMostRecentAssetStorage(request, response); + final List clusters = recentAssetManager.getRecentAssets(RecentAssetType.CLUSTER, storage.getMostRecentAssetIds(RecentAssetType.CLUSTER)); + final List views = recentAssetManager.getRecentAssets(RecentAssetType.VIEW, storage.getMostRecentAssetIds(RecentAssetType.VIEW)); + + // TODO removethese +// final Iterable clusters = clusterRepository.findAllByOrderByNameAsc(); +// final Iterable views = viewRepository.findAllByOrderByNameAsc(); model.addAttribute("MenuClusters", clusters); model.addAttribute("MenuViews", views); @@ -137,4 +157,14 @@ protected boolean hasRole(final String role) { } return false; } + + /** + * New instance of RecentAssetManager. + * @param request The current request. + * @param response The current response. + * @return RecentAssetManager instance. + */ + protected RecentAssetStorage getMostRecentAssetStorage(final HttpServletRequest request, final HttpServletResponse response) { + return new RecentAssetStorage(request, response); + } } diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/api/ApiController.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/api/ApiController.java index 1c2f4335..2798d1e9 100644 --- a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/api/ApiController.java +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/api/ApiController.java @@ -75,6 +75,8 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -636,7 +638,7 @@ private WebKafkaConsumer setup(final View view, final Collection viewOptional = viewRepository.findById(id); @@ -192,6 +198,10 @@ public String view( } final View view = viewOptional.get(); + // Add most recently used asset + getMostRecentAssetStorage(request, response) + .addMostRecentAssetId(RecentAssetType.VIEW, view.getId()); + // Setup breadcrumbs new BreadCrumbManager(model) .addCrumb("View", "/view") diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAsset.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAsset.java new file mode 100644 index 00000000..1993afc5 --- /dev/null +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAsset.java @@ -0,0 +1,35 @@ +package org.sourcelab.kafka.webview.ui.manager.ui.recentasset; + + +public class RecentAsset { + private final String name; + private final long id; + private final String url; + + public RecentAsset(final String name, final long id, final String url) { + this.name = name; + this.id = id; + this.url = url; + } + + public String getName() { + return name; + } + + public long getId() { + return id; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + return "RecentAsset{" + + "name='" + name + '\'' + + ", id=" + id + + ", url='" + url + '\'' + + '}'; + } +} diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetManager.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetManager.java new file mode 100644 index 00000000..640ad0af --- /dev/null +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetManager.java @@ -0,0 +1,71 @@ +package org.sourcelab.kafka.webview.ui.manager.ui.recentasset; + +import org.sourcelab.kafka.webview.ui.repository.ClusterRepository; +import org.sourcelab.kafka.webview.ui.repository.ViewRepository; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * + */ +public class RecentAssetManager { + private final ClusterRepository clusterRepository; + private final ViewRepository viewRepository; + + @Autowired + public RecentAssetManager(final ClusterRepository clusterRepository, final ViewRepository viewRepository) { + this.clusterRepository = Objects.requireNonNull(clusterRepository); + this.viewRepository = Objects.requireNonNull(viewRepository); + } + + public List getRecentAssets(final RecentAssetType type, final List ids) { + if (ids.isEmpty()) { + return Collections.emptyList(); + } + + switch (type) { + case CLUSTER: + return retrieveClusters(ids); + case VIEW: + return retrieveViews(ids); + default: + return Collections.emptyList(); + } + } + + private List retrieveViews(final List ids) { + final List recentAssets = new ArrayList<>(); + + for (final long id : ids) { + viewRepository.findById(id).ifPresent((view) -> { + recentAssets.add(new RecentAsset( + view.getName(), + view.getId(), + "/view/read/" + view.getId() + )); + }); + } + return Collections.unmodifiableList(recentAssets); + } + + private List retrieveClusters(final List ids) { + final List recentAssets = new ArrayList<>(); + + for (final long id : ids) { + clusterRepository.findById(id).ifPresent((cluster) -> { + recentAssets.add(new RecentAsset( + cluster.getName(), + cluster.getId(), + "/cluster/read/" + cluster.getId() + )); + }); + } + return Collections.unmodifiableList(recentAssets); + } + + +} diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetStorage.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetStorage.java new file mode 100644 index 00000000..0eeb43a6 --- /dev/null +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetStorage.java @@ -0,0 +1,159 @@ +package org.sourcelab.kafka.webview.ui.manager.ui.recentasset; + +import com.google.common.base.Charsets; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.util.WebUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Used to remember a user's most recent assets using cookies. + */ +@NotThreadSafe +public class RecentAssetStorage { + private static final Logger logger = LoggerFactory.getLogger(RecentAssetStorage.class); + + private static final int MAX_PER_ASSET = 15; + private static final Pattern FILTER_PATTERN = Pattern.compile("[^0-9, ]"); + + private final HttpServletRequest request; + private final HttpServletResponse response; + + // Cache current cookie value between request and response. + private final Map> currentCookieValues = new HashMap<>(); + + /** + * Constructor. + * @param request Request instance. + * @param response Response instance. + */ + public RecentAssetStorage(final HttpServletRequest request, final HttpServletResponse response) { + this.request = Objects.requireNonNull(request); + this.response = Objects.requireNonNull(response); + } + + /** + * Get the most recent asset ids for the given type. + * @param type Asset type. + * @return List of Ids for that asset, ordered by most recently accessed. + */ + public List getMostRecentAssetIds(final RecentAssetType type) { + Objects.requireNonNull(type); + + // Get value if available. + if (currentCookieValues.containsKey(type)) { + return currentCookieValues.get(type); + } + + // Get from cookie + final String cookieName = getCookieName(type); + final Cookie cookie = WebUtils.getCookie(request, cookieName); + + // If no cookie exists + if (cookie == null) { + // Default to new list + currentCookieValues.put(type, new ArrayList<>()); + return currentCookieValues.get(type); + } + + // Pull string value out. + final String cookieValueStrEncoded = cookie.getValue(); + + // Base64 decode + String cookieValueStr = ""; + try { + cookieValueStr = new String( + Base64.getDecoder().decode(cookieValueStrEncoded), + Charsets.UTF_8 + ); + } catch (final IllegalArgumentException exception) { + logger.error("Invalid value in cookie {} : {}", type, exception.getMessage(), exception); + cookieValueStr = ""; + } + + // Poor mans parsing + final Matcher matcher = FILTER_PATTERN.matcher(cookieValueStr); + final String cleanedStr = matcher.replaceAll("").trim(); + final List values = Arrays.stream(cleanedStr.split(",")) + .map(String::trim) + .map((val) -> { + try { + return Long.parseLong(val); + } catch (NumberFormatException exception) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // Save into map, trimming max number of entries. + currentCookieValues.put(type, new ArrayList<>( + values.subList(0, Math.min(MAX_PER_ASSET, values.size())) + )); + return currentCookieValues.get(type); + } + + /** + * Add an asset to the most recently used assets. + * @param type Type of asset. + * @param assetId Id of the asset. + * @return Updated list of most recently used assets. + */ + public List addMostRecentAssetId(final RecentAssetType type, final long assetId) { + Objects.requireNonNull(type); + + // Get the most recent list. + final List current = getMostRecentAssetIds(type); + + // Remove this asset from the list + current.remove(assetId); + + // Add this asset to the head + current.add(0, assetId); + + // Trim the list to a max value. + final List updated = current.subList(0, Math.min(MAX_PER_ASSET, current.size())); + + // Update cached value. + currentCookieValues.put(type, updated); + + // Persist as a cookie. + final Cookie cookie = createCookie(type, updated); + response.addCookie(cookie); + + return updated; + } + + private String getCookieName(final RecentAssetType type) { + Objects.requireNonNull(type); + return "recent_" + type.name(); + } + + private Cookie createCookie(final RecentAssetType type, final List ids) { + final String cookieName = getCookieName(type); + final String cookieValue = ids.toString(); + final String cookieValueEncoded = Base64.getEncoder().encodeToString(cookieValue.getBytes(Charsets.UTF_8)); + + // create a cookie + final Cookie cookie = new Cookie(cookieName, cookieValueEncoded); + cookie.setMaxAge(180 * 24 * 60 * 60); // expires in 180 days + cookie.setHttpOnly(true); + cookie.setPath("/"); + + return cookie; + } +} diff --git a/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetType.java b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetType.java new file mode 100644 index 00000000..c0d9434e --- /dev/null +++ b/kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/manager/ui/recentasset/RecentAssetType.java @@ -0,0 +1,7 @@ +package org.sourcelab.kafka.webview.ui.manager.ui.recentasset; + +public enum RecentAssetType { + UNKNOWN, + CLUSTER, + VIEW; +}