diff --git a/application-admintools-api/src/main/java/com/xwiki/admintools/health/cache/CacheInfo.java b/application-admintools-api/src/main/java/com/xwiki/admintools/health/cache/CacheInfo.java new file mode 100644 index 00000000..8876c1c0 --- /dev/null +++ b/application-admintools-api/src/main/java/com/xwiki/admintools/health/cache/CacheInfo.java @@ -0,0 +1,117 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.health.cache; + +import org.xwiki.stability.Unstable; + +/** + * Holds JMX exposed information of name, eviction limit and size for a single cache. + * + * @version $Id$ + * @since 1.3 + */ +@Unstable +public class CacheInfo +{ + private String cacheName; + + private long evictionSize; + + private long numberOfEntries; + + /** + * Default constructor. + */ + public CacheInfo() + { + + } + + /** + * {@link #setEvictionSize}. + * + * @return the eviction size + */ + public long getEvictionSize() + { + return evictionSize; + } + + /** + * Set the maximum eviction size for the cache. This represents the configured upper limit after which entries may + * be evicted. + * + * @param evictionSize the configured eviction size for the cache + */ + public void setEvictionSize(long evictionSize) + { + this.evictionSize = evictionSize; + } + + /** + * {@link #setNumberOfEntries}. + * + * @return the number of entries + */ + public long getNumberOfEntries() + { + return numberOfEntries; + } + + /** + * Set the current number of entries stored in the cache. + * + * @param numberOfEntries the current number of cache entries + */ + public void setNumberOfEntries(long numberOfEntries) + { + this.numberOfEntries = numberOfEntries; + } + + /** + * {@link #setCacheName}. + * + * @return the cache name + */ + public String getCacheName() + { + return cacheName; + } + + /** + * Set the name of the cache. + * + * @param cacheName name of the cache + */ + public void setCacheName(String cacheName) + { + this.cacheName = cacheName; + } + + /** + * Get a formatted display of the cache load level, expressed as numberOfEntries/evictionSize. + * + * @return a formatted display of the cache size + */ + public String getFormattedCacheSize() + { + return String.format("%d/%d", numberOfEntries, evictionSize); + } +} diff --git a/application-admintools-api/src/main/java/com/xwiki/admintools/rest/AdminToolsResource.java b/application-admintools-api/src/main/java/com/xwiki/admintools/rest/AdminToolsResource.java index aae3b8f9..e4352ef6 100644 --- a/application-admintools-api/src/main/java/com/xwiki/admintools/rest/AdminToolsResource.java +++ b/application-admintools-api/src/main/java/com/xwiki/admintools/rest/AdminToolsResource.java @@ -24,6 +24,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import org.xwiki.rest.XWikiRestComponent; @@ -74,16 +75,49 @@ public interface AdminToolsResource extends XWikiRestComponent * * @param attachReference the reference of the attachment. * @param jobId a unique id for the job. - * @return HTML status code 202 to hint that the upload job has started; - * Return status code 102 if the job already exists and is in progress; - * Return status code 401 if the user does not have admin rights; - * Return status code 500 if there is any error. + * @return HTML status code 202 to hint that the upload job has started; Return status code 102 if the job already + * exists and is in progress; Return status code 401 if the user does not have admin rights; Return status code + * 500 if there is any error. * @throws XWikiRestException if an error occurred while creating the job, or if the user lacks admin rights. * @since 1.1 */ @POST @Path("/upload") @Unstable - Response uploadPackageArchive(@QueryParam("attach") String attachReference, - @QueryParam("jobId") String jobId) throws XWikiRestException; + default Response uploadPackageArchive(@QueryParam("attach") String attachReference, + @QueryParam("jobId") String jobId) throws XWikiRestException + { + throw new WebApplicationException(501); + } + + /** + * Flush all JMX managed caches. + * + * @return the status of the operation + * @throws XWikiRestException if an error occurred while flushing the cache + * @since 1.3 + */ + @POST + @Path("/flushCache/jmx") + @Unstable + default Response flushJMXCache() throws XWikiRestException + { + throw new WebApplicationException(501); + } + + /** + * Flush a single JMX managed cache identified by its name. + * + * @param cacheName the name of the cache to flush + * @return the status of the operation + * @throws XWikiRestException if an error occurred while flushing the cache + * @since 1.3 + */ + @POST + @Path("/flushCache/jmx/cache") + @Unstable + default Response flushJMXEntryCache(@QueryParam("cacheName") String cacheName) throws XWikiRestException + { + throw new WebApplicationException(501); + } } diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/AdminToolsManager.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/AdminToolsManager.java index 31459507..5e9c27cb 100644 --- a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/AdminToolsManager.java +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/AdminToolsManager.java @@ -194,4 +194,16 @@ public SolrDocumentList getEmptyDocuments(Map filters, String or { return this.instanceUsageManager.getEmptyDocuments(filters, order); } + + /** + * Check if the used server is compatible with Admin tools installation. + * + * @return a {@code true} if the used server is compatible with the application installation, or {@code false} + * otherwise. + * @since 1.0 + */ + public boolean isUsedServerCompatible() + { + return currentServer.getCurrentServer() != null; + } } diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/CacheManager.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/CacheManager.java new file mode 100644 index 00000000..899a17f9 --- /dev/null +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/CacheManager.java @@ -0,0 +1,89 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache; + +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.management.JMException; +import javax.management.ObjectName; + +import org.xwiki.component.annotation.Component; + +import com.xwiki.admintools.health.cache.CacheInfo; +import com.xwiki.admintools.internal.health.cache.data.CacheDataGenerator; + +import groovy.jmx.GroovyMBean; + +/** + * Manager class that handles JMX managed cache operations. + * + * @version $Id$ + * @since 1.3 + */ +@Component(roles = CacheManager.class) +@Singleton +public class CacheManager +{ + @Inject + private CacheDataGenerator cacheDataGenerator; + + /** + * Get a sorted and filtered {@code List} with the JMX managed caches. + * + * @param filter used to filter caches by name + * @param order the sort order applied to the result, based on the number of entries + * @return a sorted and filtered {@code List} with the JMX managed caches + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public List getJMXCaches(String filter, String order) throws JMException, IOException + { + List cacheEntries = cacheDataGenerator.getCacheEntries(filter); + return getSortedAndFilteredCacheEntries(cacheEntries, order); + } + + /** + * Get detailed statistics for a specific cache. + * + * @param name cache name to be searched for + * @return a {@link Map} with the detailed statistics + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public Map getCacheDetailedView(String name) throws JMException, IOException + { + return cacheDataGenerator.getDetailedCacheEntry(name); + } + + private List getSortedAndFilteredCacheEntries(List cacheEntries, String order) + { + + boolean descending = order.equals("desc"); + return cacheEntries.stream().sorted( + descending ? Comparator.comparingLong(CacheInfo::getNumberOfEntries).reversed() + : Comparator.comparingLong(CacheInfo::getNumberOfEntries)).collect(Collectors.toList()); + } +} diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/GroovyMBeanUtil.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/GroovyMBeanUtil.java new file mode 100644 index 00000000..7a2281ab --- /dev/null +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/GroovyMBeanUtil.java @@ -0,0 +1,69 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache; + +import java.io.IOException; +import java.lang.management.ManagementFactory; + +import javax.inject.Singleton; +import javax.management.JMException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.xwiki.component.annotation.Component; + +import groovy.jmx.GroovyMBean; + +/** + * Utility class used to access JMX MBeans. + * + * @version $Id$ + * @since 1.3 + */ +@Component(roles = GroovyMBeanUtil.class) +@Singleton +public class GroovyMBeanUtil +{ + private MBeanServer mBeanServer; + + /** + * Return {@link MBeanServer}. + * + * @return the platform MBean server + */ + public MBeanServer getMBeanServer() + { + if (mBeanServer == null) { + mBeanServer = ManagementFactory.getPlatformMBeanServer(); + } + return mBeanServer; + } + + /** + * Create a {@link GroovyMBean} wrapper for a given {@link ObjectName}. + * + * @param objectName the JMX object name identifying the MBean + * @return a {@link GroovyMBean} instance for the given object name + */ + public GroovyMBean getGroovyMBean(ObjectName objectName) throws JMException, IOException + { + return new GroovyMBean(getMBeanServer(), objectName); + } +} diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusher.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusher.java new file mode 100644 index 00000000..17e33c17 --- /dev/null +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusher.java @@ -0,0 +1,108 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.io.IOException; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.management.JMException; +import javax.management.ObjectName; + +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; + +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +import groovy.jmx.GroovyMBean; + +import static com.xwiki.admintools.internal.health.cache.data.CacheDataUtil.NAME_KEY; + +/** + * Handles the flush of JMX managed cache. + * + * @version $Id$ + * @since 1.3 + */ +@Component(roles = CacheDataFlusher.class) +@Singleton +public class CacheDataFlusher +{ + private static final String QUERY_CACHE_TARGET = "Cache"; + + private static final String CLEAR_METHOD_KEY = "clear"; + + @Inject + private Logger logger; + + @Inject + private GroovyMBeanUtil groovyMBeanUtil; + + @Inject + private CacheDataUtil cacheDataUtil; + + /** + * Flush all JMX managed cache. + * + * @return {@code true} if all cache was flushed successfully, or {@code false} otherwise + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public boolean clearAllCache() throws JMException, IOException + { + Set cacheSet = cacheDataUtil.getCacheSet(QUERY_CACHE_TARGET); + boolean noError = true; + for (ObjectName cache : cacheSet) { + GroovyMBean groovyMBean = groovyMBeanUtil.getGroovyMBean(cache); + try { + groovyMBean.invokeMethod(CLEAR_METHOD_KEY, new Object[0]); + } catch (Exception e) { + String errMessage = String.format("There was an error while flushing the cache for [%s]", + cache.getKeyProperty(NAME_KEY)); + logger.error(errMessage, e); + noError = false; + } + } + return noError; + } + + /** + * Flush a specific cache. + * + * @param cacheName the target cache that needs to be flushed + * @return {@code true} if the given cache name was found, or {@code false} otherwise + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public boolean clearCache(String cacheName) throws JMException, IOException + { + Set cacheSet = cacheDataUtil.getCacheSet(QUERY_CACHE_TARGET); + for (ObjectName cache : cacheSet) { + GroovyMBean groovyMBean = groovyMBeanUtil.getGroovyMBean(cache); + String name = cache.getKeyProperty(NAME_KEY).replace("\"", ""); + if (name.equals(cacheName)) { + groovyMBean.invokeMethod(CLEAR_METHOD_KEY, new Object[0]); + return true; + } + } + return false; + } +} diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGenerator.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGenerator.java new file mode 100644 index 00000000..0fd83d7b --- /dev/null +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGenerator.java @@ -0,0 +1,153 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.management.JMException; +import javax.management.ObjectName; + +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; + +import com.xwiki.admintools.health.cache.CacheInfo; +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +import groovy.jmx.GroovyMBean; + +import static com.xwiki.admintools.internal.health.cache.data.CacheDataUtil.NAME_KEY; + +/** + * Handles the retrieval of cache info. + * + * @version $Id$ + * @since 1.3 + */ +@Component(roles = CacheDataGenerator.class) +@Singleton +public class CacheDataGenerator +{ + private static final String QUERY_CONFIGURATION_TARGET = "Configuration"; + + private static final String QUERY_STATISTICS_TARGET = "Statistics"; + + private static final String REGEX = "\""; + + @Inject + private Logger logger; + + @Inject + private GroovyMBeanUtil groovyMBeanUtil; + + @Inject + private CacheDataUtil cacheDataUtil; + + /** + * Get a {@link List} with all the JMX managed cache. + * + * @param filter used to filter caches by name + * @return a {@link List} of {@link CacheInfo} + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public List getCacheEntries(String filter) throws JMException, IOException + { + List cacheEntries = new ArrayList<>(); + String filterValue = filter == null ? "" : filter; + Set configCacheSet = cacheDataUtil.getCacheSet(QUERY_CONFIGURATION_TARGET); + Map statsMap = loadStatisticsMap(); + + for (ObjectName configCache : configCacheSet) { + String cacheName = configCache.getKeyProperty(NAME_KEY); + + if (cacheName.contains(filterValue)) { + GroovyMBean groovyMBean = groovyMBeanUtil.getGroovyMBean(configCache); + long evictionSize = ((Number) groovyMBean.getProperty("evictionSize")).longValue(); + long numberOfEntries = statsMap.getOrDefault(cacheName, -1L); + + CacheInfo cacheInfo = new CacheInfo(); + cacheInfo.setCacheName(cacheName.replace(REGEX, "")); + cacheInfo.setEvictionSize(evictionSize); + cacheInfo.setNumberOfEntries(numberOfEntries); + cacheEntries.add(cacheInfo); + } + } + return cacheEntries; + } + + /** + * Get a {@link Map} with all the statistics for a given cache. + * + * @param name searched cache name + * @return the full statistics of a given cache + * @throws JMException if there are any errors during {@link ObjectName} or {@link GroovyMBean} creation + * @throws IOException if there are any errors during the {@link GroovyMBean} creation + */ + public Map getDetailedCacheEntry(String name) throws JMException, IOException + { + Map detailedEntryMap = new HashMap<>(); + Set statsCacheSet = cacheDataUtil.getCacheSet(QUERY_STATISTICS_TARGET); + for (ObjectName statsCache : statsCacheSet) { + GroovyMBean bean = groovyMBeanUtil.getGroovyMBean(statsCache); + String cacheName = statsCache.getKeyProperty(NAME_KEY); + if (cacheName.replace(REGEX, "").equals(name)) { + for (String attribute : bean.listAttributeNames()) { + try { + Object propertyValue = bean.getProperty(attribute); + detailedEntryMap.put(attribute, propertyValue); + } catch (Exception e) { + logger.warn(String.format("Failed to retrieve attribute [%s] for cache [%s].", attribute, name), + e); + } + } + break; + } + } + return detailedEntryMap; + } + + private Map loadStatisticsMap() throws JMException, IOException + { + Map statsMap = new HashMap<>(); + Set statsNames = cacheDataUtil.getCacheSet(QUERY_STATISTICS_TARGET); + + for (ObjectName objectName : statsNames) { + GroovyMBean bean = groovyMBeanUtil.getGroovyMBean(objectName); + String name = objectName.getKeyProperty(NAME_KEY); + try { + long count = ((Number) bean.getProperty("numberOfEntries")).longValue(); + if (count == -1) { + count = ((Number) bean.getProperty("approximateEntries")).longValue(); + } + statsMap.put(name, count); + } catch (Exception e) { + logger.warn(String.format("Failed to retrieve number of entries for cache [%s].", name), e); + } + } + return statsMap; + } +} diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtil.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtil.java new file mode 100644 index 00000000..c4fe5673 --- /dev/null +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtil.java @@ -0,0 +1,70 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.xwiki.component.annotation.Component; + +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +/** + * Abstract class for common code needed in JMX managed cache data handling. + * + * @version $Id$ + * @since 1.3 + */ +@Component(roles = CacheDataUtil.class) +@Singleton +public class CacheDataUtil +{ + /** + * Key used for the name field. + */ + public static final String NAME_KEY = "name"; + + @Inject + private GroovyMBeanUtil groovyMBeanUtil; + + /** + * Get the cache for a given component. + * + * @param target target component for which to retrieve the cache + * @return a {@link Set} with the cache objects for the given target component + * @throws MalformedObjectNameException if the string passed as a parameter does not have the right format + */ + public Set getCacheSet(String target) throws MalformedObjectNameException + { + MBeanServer server = groovyMBeanUtil.getMBeanServer(); + ObjectName queryStatistics = new ObjectName(getFormattedQuery(target)); + return server.queryNames(queryStatistics, null); + } + + private String getFormattedQuery(String target) + { + return String.format("org.xwiki.infinispan:component=%s,*", target); + } +} diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/network/HttpClientBuilderFactory.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/network/HttpClientBuilderFactory.java index 01c15463..a9711678 100644 --- a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/network/HttpClientBuilderFactory.java +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/network/HttpClientBuilderFactory.java @@ -37,8 +37,8 @@ @Unstable public class HttpClientBuilderFactory { - private HttpClient httpClient; + /** * Creates a HttpClient. * diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResource.java b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResource.java index 0b0aa3d4..7fbf193b 100644 --- a/application-admintools-default/src/main/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResource.java +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResource.java @@ -46,6 +46,7 @@ import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.web.XWikiRequest; import com.xwiki.admintools.internal.files.ImportantFilesManager; +import com.xwiki.admintools.internal.health.cache.data.CacheDataFlusher; import com.xwiki.admintools.internal.uploadJob.UploadJob; import com.xwiki.admintools.jobs.JobResult; import com.xwiki.admintools.jobs.JobResultLevel; @@ -77,6 +78,9 @@ public class DefaultAdminToolsResource extends ModifiablePageResource implements @Inject private ContextualAuthorizationManager contextualAuthorizationManager; + @Inject + private CacheDataFlusher cacheDataFlusher; + @Override public Response getFile(String hint) { @@ -145,6 +149,50 @@ public Response flushCache() } } + @Override + public Response flushJMXCache() + { + try { + this.contextualAuthorizationManager.checkAccess(Right.ADMIN); + this.contextualAuthorizationManager.checkAccess(Right.PROGRAM); + boolean success = cacheDataFlusher.clearAllCache(); + if (success) { + return Response.ok().build(); + } else { + logger.warn("There were some errors while flushing the JMX cache."); + return Response.ok().type(MediaType.TEXT_PLAIN_TYPE).build(); + } + } catch (AccessDeniedException deniedException) { + logger.warn("Failed to flush JMX caches due to restricted rights.", deniedException); + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } catch (Exception e) { + logger.warn("Failed to flush JMX caches. Root cause: [{}]", ExceptionUtils.getRootCauseMessage(e)); + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + + @Override + public Response flushJMXEntryCache(String entryName) + { + try { + this.contextualAuthorizationManager.checkAccess(Right.ADMIN); + this.contextualAuthorizationManager.checkAccess(Right.PROGRAM); + boolean found = cacheDataFlusher.clearCache(entryName); + if (found) { + return Response.ok().build(); + } else { + logger.warn("[{}] JMX cache not found.", entryName); + return Response.status(Response.Status.NOT_FOUND).build(); + } + } catch (AccessDeniedException deniedException) { + logger.warn("Failed to flush JMX cache due to restricted rights.", deniedException); + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } catch (Exception e) { + logger.warn("Failed to flush JMX cache. Root cause: [{}]", ExceptionUtils.getRootCauseMessage(e)); + throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); + } + } + @Override public Response uploadPackageArchive(String attachReference, String startTime) { diff --git a/application-admintools-default/src/main/java/com/xwiki/admintools/script/AdminToolsScriptService.java b/application-admintools-default/src/main/java/com/xwiki/admintools/script/AdminToolsScriptService.java index 94cab7fe..ea29a56b 100644 --- a/application-admintools-default/src/main/java/com/xwiki/admintools/script/AdminToolsScriptService.java +++ b/application-admintools-default/src/main/java/com/xwiki/admintools/script/AdminToolsScriptService.java @@ -45,8 +45,9 @@ import org.xwiki.wiki.manager.WikiManagerException; import com.xwiki.admintools.configuration.AdminToolsConfiguration; +import com.xwiki.admintools.health.cache.CacheInfo; import com.xwiki.admintools.internal.AdminToolsManager; -import com.xwiki.admintools.internal.data.identifiers.CurrentServer; +import com.xwiki.admintools.internal.health.cache.CacheManager; import com.xwiki.admintools.internal.health.job.HealthCheckJob; import com.xwiki.admintools.internal.network.NetworkManager; import com.xwiki.admintools.internal.security.CheckSecurityCache; @@ -87,15 +88,46 @@ public class AdminToolsScriptService implements ScriptService @Inject private EntityRightsProvider entityRightsProvider; - @Inject - private CurrentServer currentServer; - @Inject private NetworkManager networkManager; @Inject private CheckSecurityCache checkSecurityCache; + @Inject + private CacheManager cacheManager; + + /** + * Get a sorted and filtered {@code List} with the JMX managed caches. + * + * @param filter used to filter caches by name + * @param order the sort order applied to the result, based on the number of entries + * @return a sorted and filtered {@code List} with the JMX managed caches + * @throws AccessDeniedException if the requesting user lacks admin rights. + * @since 1.3 + */ + @Unstable + public List getJMXCache(String filter, String order) throws Exception + { + this.contextualAuthorizationManager.checkAccess(Right.ADMIN); + return cacheManager.getJMXCaches(filter, order); + } + + /** + * Get detailed statistics for a specific cache. + * + * @param name cache name to be searched for + * @return a {@link Map} with the detailed statistics + * @throws AccessDeniedException if the requesting user lacks admin rights. + * @since 1.3 + */ + @Unstable + public Map getDetailedCacheData(String name) throws Exception + { + this.contextualAuthorizationManager.checkAccess(Right.ADMIN); + return cacheManager.getCacheDetailedView(name); + } + /** * Retrieve JSON data from the given network endpoint. * @@ -366,7 +398,7 @@ public List getHealthCheckJobId() throws AccessDeniedException @Unstable public boolean isUsedServerCompatible() { - return currentServer.getCurrentServer() != null; + return adminToolsManager.isUsedServerCompatible(); } /** diff --git a/application-admintools-default/src/main/resources/META-INF/components.txt b/application-admintools-default/src/main/resources/META-INF/components.txt index ad32a371..37ec2959 100644 --- a/application-admintools-default/src/main/resources/META-INF/components.txt +++ b/application-admintools-default/src/main/resources/META-INF/components.txt @@ -14,6 +14,11 @@ com.xwiki.admintools.internal.configuration.AdminToolsConfigurationSource com.xwiki.admintools.internal.configuration.DefaultAdminToolsConfiguration com.xwiki.admintools.internal.AdminToolsEventListener com.xwiki.admintools.internal.AdminToolsUninstallListener +com.xwiki.admintools.internal.health.cache.CacheManager +com.xwiki.admintools.internal.health.cache.data.CacheDataGenerator +com.xwiki.admintools.internal.health.cache.data.CacheDataFlusher +com.xwiki.admintools.internal.health.cache.data.CacheDataUtil +com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil com.xwiki.admintools.internal.health.checks.configuration.ConfigurationDatabaseHealthCheck com.xwiki.admintools.internal.health.checks.configuration.ConfigurationJavaHealthCheck com.xwiki.admintools.internal.health.checks.configuration.ConfigurationOSHealthCheck diff --git a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/CacheManagerTest.java b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/CacheManagerTest.java new file mode 100644 index 00000000..4ca4ef29 --- /dev/null +++ b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/CacheManagerTest.java @@ -0,0 +1,90 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.management.JMException; + +import org.junit.jupiter.api.Test; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xwiki.admintools.health.cache.CacheInfo; +import com.xwiki.admintools.internal.health.cache.data.CacheDataGenerator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link CacheManager} + * + * @version $Id$ + */ +@ComponentTest +class CacheManagerTest +{ + @InjectMockComponents + private CacheManager cacheManager; + + @MockComponent + private CacheDataGenerator dataGenerator; + + @Test + void getJMXCachesTest() throws JMException, IOException + { + String filter = "filter"; + CacheInfo cacheInfo1 = new CacheInfo(); + CacheInfo cacheInfo2 = new CacheInfo(); + CacheInfo cacheInfo3 = new CacheInfo(); + cacheInfo1.setNumberOfEntries(100L); + cacheInfo2.setNumberOfEntries(50L); + cacheInfo3.setNumberOfEntries(75L); + + List cacheInfoList = new ArrayList<>(3); + cacheInfoList.add(cacheInfo1); + cacheInfoList.add(cacheInfo2); + cacheInfoList.add(cacheInfo3); + + when(dataGenerator.getCacheEntries(filter)).thenReturn(cacheInfoList); + + List results = cacheManager.getJMXCaches(filter, "desc"); + assertEquals(cacheInfo1, results.get(0)); + assertEquals(cacheInfo3, results.get(1)); + assertEquals(cacheInfo2, results.get(2)); + + results = cacheManager.getJMXCaches(filter, "asc"); + assertEquals(cacheInfo2, results.get(0)); + assertEquals(cacheInfo3, results.get(1)); + assertEquals(cacheInfo1, results.get(2)); + } + + @Test + void getCacheDetailedViewTest() throws JMException, IOException + { + String name = "some cache name"; + when(dataGenerator.getDetailedCacheEntry(name)).thenReturn(Map.of("key", 1)); + assertEquals(1, cacheManager.getCacheDetailedView(name).get("key")); + } +} diff --git a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusherTest.java b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusherTest.java new file mode 100644 index 00000000..e1b7a131 --- /dev/null +++ b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataFlusherTest.java @@ -0,0 +1,140 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.io.IOException; +import java.util.Set; + +import javax.management.JMException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +import groovy.jmx.GroovyMBean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link CacheDataFlusher} + * + * @version $Id$ + */ +@ComponentTest +class CacheDataFlusherTest +{ + @InjectMockComponents + private CacheDataFlusher cacheDataFlusher; + + @MockComponent + private GroovyMBeanUtil groovyMBeanUtil; + + @MockComponent + private CacheDataUtil dataUtil; + + @Mock + private ObjectName objectName1; + + @Mock + private ObjectName objectName2; + + @Mock + private ObjectName objectName3; + + @Mock + private GroovyMBean groovyMBean1; + + @Mock + private GroovyMBean groovyMBean2; + + @Mock + private GroovyMBean groovyMBean3; + + @RegisterExtension + private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.ERROR); + + @Test + void clearAllCacheTest() throws JMException, IOException + { + when(dataUtil.getCacheSet("Cache")).thenReturn(Set.of(objectName1, objectName2, objectName3)); + when(groovyMBeanUtil.getGroovyMBean(objectName1)).thenReturn(groovyMBean1); + when(groovyMBeanUtil.getGroovyMBean(objectName2)).thenReturn(groovyMBean2); + when(groovyMBeanUtil.getGroovyMBean(objectName3)).thenReturn(groovyMBean3); + + when(groovyMBean2.invokeMethod(eq("clear"), any(Object.class))).thenThrow(new RuntimeException("failed")); + when(objectName2.getKeyProperty("name")).thenReturn("name 2"); + + assertFalse(cacheDataFlusher.clearAllCache()); + verify(groovyMBean1, times(1)).invokeMethod(eq("clear"), any(Object.class)); + verify(groovyMBean2, times(1)).invokeMethod(eq("clear"), any(Object.class)); + assertEquals("There was an error while flushing the cache for [name 2]", logCapture.getMessage(0)); + } + + @Test + void clearCacheFound() throws JMException, IOException + { + when(dataUtil.getCacheSet("Cache")).thenReturn(Set.of(objectName1, objectName2, objectName3)); + when(groovyMBeanUtil.getGroovyMBean(objectName1)).thenReturn(groovyMBean1); + when(groovyMBeanUtil.getGroovyMBean(objectName2)).thenReturn(groovyMBean2); + when(groovyMBeanUtil.getGroovyMBean(objectName3)).thenReturn(groovyMBean3); + + when(objectName1.getKeyProperty("name")).thenReturn("name 1"); + when(objectName2.getKeyProperty("name")).thenReturn("name 2"); + when(objectName3.getKeyProperty("name")).thenReturn("name 3"); + + assertTrue(cacheDataFlusher.clearCache("name 2")); + verify(groovyMBean1, times(0)).invokeMethod(eq("clear"), any(Object.class)); + verify(groovyMBean2, times(1)).invokeMethod(eq("clear"), any(Object.class)); + verify(groovyMBean3, times(0)).invokeMethod(eq("clear"), any(Object.class)); + } + + @Test + void clearCacheNotFound() throws JMException, IOException + { + when(dataUtil.getCacheSet("Cache")).thenReturn(Set.of(objectName1, objectName2, objectName3)); + when(groovyMBeanUtil.getGroovyMBean(objectName1)).thenReturn(groovyMBean1); + when(groovyMBeanUtil.getGroovyMBean(objectName2)).thenReturn(groovyMBean2); + when(groovyMBeanUtil.getGroovyMBean(objectName3)).thenReturn(groovyMBean3); + + when(objectName1.getKeyProperty("name")).thenReturn("name 1"); + when(objectName2.getKeyProperty("name")).thenReturn("name 2"); + when(objectName3.getKeyProperty("name")).thenReturn("name 3"); + + assertFalse(cacheDataFlusher.clearCache("name 4")); + verify(groovyMBean1, times(0)).invokeMethod(eq("clear"), any(Object.class)); + verify(groovyMBean2, times(0)).invokeMethod(eq("clear"), any(Object.class)); + verify(groovyMBean3, times(0)).invokeMethod(eq("clear"), any(Object.class)); + } +} diff --git a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGeneratorTest.java b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGeneratorTest.java new file mode 100644 index 00000000..cdf733d5 --- /dev/null +++ b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataGeneratorTest.java @@ -0,0 +1,173 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.JMException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.xwiki.test.LogLevel; +import org.xwiki.test.junit5.LogCaptureExtension; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xwiki.admintools.health.cache.CacheInfo; +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +import groovy.jmx.GroovyMBean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link CacheDataGenerator} + * + * @version $Id$ + */ +@ComponentTest +class CacheDataGeneratorTest +{ + @InjectMockComponents + private CacheDataGenerator cacheDataGenerator; + + @MockComponent + private GroovyMBeanUtil groovyMBeanUtil; + + @MockComponent + private CacheDataUtil dataUtil; + + @Mock + private ObjectName objectName1; + + @Mock + private ObjectName objectName2; + + @Mock + private ObjectName objectName3; + + @Mock + private ObjectName objectStats1; + + @Mock + private ObjectName objectStats2; + + @Mock + private ObjectName objectStats3; + + @Mock + private GroovyMBean groovyMBean1; + + @Mock + private GroovyMBean groovyMBean2; + + @Mock + private GroovyMBean groovyMBean3; + + @Mock + private GroovyMBean groovyMBean4; + + @Mock + private GroovyMBean groovyMBean5; + + @Mock + private GroovyMBean groovyMBean6; + + @RegisterExtension + private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); + + @Test + void getCacheEntriesTest() throws JMException, IOException + { + Set configs = new LinkedHashSet<>(); + configs.add(objectName1); + configs.add(objectName2); + configs.add(objectName3); + when(dataUtil.getCacheSet("Configuration")).thenReturn(configs); + when(dataUtil.getCacheSet("Statistics")).thenReturn(Set.of(objectStats1, objectStats2, objectStats3)); + + when(objectName1.getKeyProperty("name")).thenReturn("\"filter key 1\""); + when(objectName2.getKeyProperty("name")).thenReturn("\"filter key 2\""); + when(objectName3.getKeyProperty("name")).thenReturn("\"key 3\""); + when(groovyMBeanUtil.getGroovyMBean(objectName1)).thenReturn(groovyMBean1); + when(groovyMBeanUtil.getGroovyMBean(objectName2)).thenReturn(groovyMBean2); + when(groovyMBeanUtil.getGroovyMBean(objectName3)).thenReturn(groovyMBean3); + + when(objectStats1.getKeyProperty("name")).thenReturn("\"filter key 1\""); + when(objectStats2.getKeyProperty("name")).thenReturn("\"filter key\""); + when(objectStats3.getKeyProperty("name")).thenReturn("\"key 3\""); + when(groovyMBeanUtil.getGroovyMBean(objectStats1)).thenReturn(groovyMBean4); + when(groovyMBeanUtil.getGroovyMBean(objectStats2)).thenReturn(groovyMBean5); + when(groovyMBeanUtil.getGroovyMBean(objectStats3)).thenReturn(groovyMBean6); + + when(groovyMBean1.getProperty("evictionSize")).thenReturn(20L); + when(groovyMBean2.getProperty("evictionSize")).thenReturn(50L); + when(groovyMBean3.getProperty("evictionSize")).thenReturn(80L); + + when(groovyMBean4.getProperty("numberOfEntries")).thenReturn(10L); + when(groovyMBean5.getProperty("numberOfEntries")).thenThrow(new RuntimeException("Could not access property")); + when(groovyMBean6.getProperty("numberOfEntries")).thenReturn(-1L); + when(groovyMBean6.getProperty("approximateEntries")).thenReturn(28L); + + List results = cacheDataGenerator.getCacheEntries("ilter"); + assertEquals("Failed to retrieve number of entries for cache [\"filter key\"].", logCapture.getMessage(0)); + assertEquals(2, results.size()); + assertEquals("filter key 1", results.get(0).getCacheName()); + assertEquals("10/20", results.get(0).getFormattedCacheSize()); + + assertEquals("filter key 2", results.get(1).getCacheName()); + assertEquals("-1/50", results.get(1).getFormattedCacheSize()); + } + + @Test + void getDetailedCacheEntry() throws JMException, IOException + { + when(dataUtil.getCacheSet("Statistics")).thenReturn(Set.of(objectStats1, objectStats2, objectStats3)); + when(groovyMBeanUtil.getGroovyMBean(objectStats1)).thenReturn(groovyMBean4); + when(groovyMBeanUtil.getGroovyMBean(objectStats2)).thenReturn(groovyMBean5); + when(groovyMBeanUtil.getGroovyMBean(objectStats3)).thenReturn(groovyMBean6); + + when(objectStats1.getKeyProperty("name")).thenReturn("\"filter key 1\""); + when(objectStats2.getKeyProperty("name")).thenReturn("\"filter key\""); + when(objectStats3.getKeyProperty("name")).thenReturn("\"key 3\""); + when(groovyMBean5.listAttributeNames()).thenReturn( + List.of("approximateEntries", "numberOfEntries", "evictionSize")); + + when(groovyMBean5.getProperty("numberOfEntries")).thenThrow(new RuntimeException("Could not access property")); + when(groovyMBean5.getProperty("approximateEntries")).thenReturn(28L); + when(groovyMBean5.getProperty("evictionSize")).thenReturn(80L); + + Map results = cacheDataGenerator.getDetailedCacheEntry("filter key"); + assertEquals(2, results.size()); + assertEquals(28L, results.get("approximateEntries")); + assertEquals(80L, results.get("evictionSize")); + assertEquals("empty", results.getOrDefault("numberOfEntries", "empty")); + assertEquals("Failed to retrieve attribute [numberOfEntries] for cache [filter key].", + logCapture.getMessage(0)); + } +} diff --git a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtilTest.java b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtilTest.java new file mode 100644 index 00000000..e6a4086c --- /dev/null +++ b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/health/cache/data/CacheDataUtilTest.java @@ -0,0 +1,71 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package com.xwiki.admintools.internal.health.cache.data; + +import java.util.Set; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xwiki.admintools.internal.health.cache.GroovyMBeanUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * Unit test for {@link CacheDataUtil} + * + * @version $Id$ + */ +@ComponentTest +class CacheDataUtilTest +{ + @InjectMockComponents + private CacheDataUtil cacheDataUtil; + + @MockComponent + private GroovyMBeanUtil mBeanUtil; + + @Mock + private MBeanServer mBeanServer; + + @Mock + private ObjectName objectName; + + @Test + void getCacheSet() throws MalformedObjectNameException + { + when(mBeanUtil.getMBeanServer()).thenReturn(mBeanServer); + Set objectNameSet = Set.of(objectName); + + when(mBeanServer.queryNames(any(ObjectName.class), eq(null))).thenReturn(objectNameSet); + + assertEquals(objectNameSet, cacheDataUtil.getCacheSet("test")); + } +} diff --git a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResourceTest.java b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResourceTest.java index 9621d358..344731fa 100644 --- a/application-admintools-default/src/test/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResourceTest.java +++ b/application-admintools-default/src/test/java/com/xwiki/admintools/internal/rest/DefaultAdminToolsResourceTest.java @@ -25,13 +25,13 @@ import java.util.Map; import javax.inject.Provider; +import javax.management.JMException; import javax.ws.rs.WebApplicationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mock; -import org.slf4j.Logger; -import org.xwiki.component.util.ReflectionUtils; import org.xwiki.job.Job; import org.xwiki.job.JobException; import org.xwiki.job.JobExecutor; @@ -41,7 +41,9 @@ import org.xwiki.security.authorization.AccessDeniedException; import org.xwiki.security.authorization.ContextualAuthorizationManager; import org.xwiki.security.authorization.Right; +import org.xwiki.test.LogLevel; import org.xwiki.test.annotation.BeforeComponent; +import org.xwiki.test.junit5.LogCaptureExtension; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; @@ -51,6 +53,7 @@ import com.xpn.xwiki.web.XWikiRequest; import com.xwiki.admintools.internal.files.ImportantFilesManager; import com.xwiki.admintools.internal.files.resources.logs.LogsDataResource; +import com.xwiki.admintools.internal.health.cache.data.CacheDataFlusher; import com.xwiki.admintools.internal.uploadJob.UploadJob; import com.xwiki.admintools.jobs.JobResult; import com.xwiki.admintools.jobs.PackageUploadJobRequest; @@ -96,14 +99,17 @@ class DefaultAdminToolsResourceTest @MockComponent private JobExecutor jobExecutor; + @MockComponent + private CacheDataFlusher cacheDataFlusher; + @Mock private DocumentReference user; @Mock private WikiReference wikiReference; - @Mock - private Logger logger; + @RegisterExtension + private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); @Mock private XWiki xwiki; @@ -145,42 +151,36 @@ void getFile() throws Exception @Test void getFileNotFound() throws Exception { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); - when(importantFilesManager.getFile("resource_hint", params)).thenThrow(new IOException("FILE NOT FOUND")); assertEquals(404, defaultAdminToolsResource.getFile("resource_hint").getStatus()); - verify(logger).warn("Error while handling file from DataResource [{}]. Root cause: [{}]", "resource_hint", - "IOException: FILE NOT FOUND"); + assertEquals( + "Error while handling file from DataResource [resource_hint]. Root cause: [IOException: FILE NOT FOUND]", + logCapture.getMessage(0)); } @Test void getFileDownloadManagerError() { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); - WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { this.defaultAdminToolsResource.getFile("resource_hint"); }); assertEquals(500, exception.getResponse().getStatus()); - verify(logger).warn("Failed to get data from DataResource [{}]. Root cause: [{}]", "resource_hint", - "NullPointerException: "); + assertEquals("Failed to get data from DataResource [resource_hint]. Root cause: [NullPointerException: ]", + logCapture.getMessage(0)); } @Test void getFileNotAdmin() throws AccessDeniedException { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); doThrow(new AccessDeniedException(Right.ADMIN, user, null)).when(contextualAuthorizationManager) .checkAccess(Right.ADMIN); WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { this.defaultAdminToolsResource.getFile("resource_hint"); }); assertEquals(401, exception.getResponse().getStatus()); - verify(logger).warn("Failed to get file from DataResource [{}] due to restricted rights.", "resource_hint"); + assertEquals("Failed to get file from DataResource [resource_hint] due to restricted rights.", + logCapture.getMessage(0)); } @Test @@ -196,9 +196,6 @@ void getFiles() throws Exception @Test void getFilesDownloadManagerError() throws Exception { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); - Map formParameters = new HashMap<>(); when(xWikiRequest.getParameterMap()).thenReturn(formParameters); when(importantFilesManager.getFilesArchive(formParameters)).thenThrow( @@ -207,21 +204,20 @@ void getFilesDownloadManagerError() throws Exception defaultAdminToolsResource.getFiles(); }); assertEquals(500, exception.getResponse().getStatus()); - verify(logger).warn("Failed to get zip archive. Root cause: [{}]", "Exception: DOWNLOAD MANAGER EXCEPTION"); + assertEquals("Failed to get zip archive. Root cause: [Exception: DOWNLOAD MANAGER EXCEPTION]", + logCapture.getMessage(0)); } @Test void getFilesNotAdmin() throws AccessDeniedException { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); doThrow(new AccessDeniedException(Right.ADMIN, user, null)).when(contextualAuthorizationManager) .checkAccess(Right.ADMIN); WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { this.defaultAdminToolsResource.getFiles(); }); assertEquals(401, exception.getResponse().getStatus()); - verify(logger).warn("Failed to get files due to restricted rights."); + assertEquals("Failed to get files due to restricted rights.", logCapture.getMessage(0)); } @Test @@ -242,15 +238,13 @@ void getLastLogsNoInput() throws Exception @Test void flushCacheNoRights() throws AccessDeniedException { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); doThrow(new AccessDeniedException(Right.ADMIN, user, null)).when(contextualAuthorizationManager) .checkAccess(Right.ADMIN); WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { this.defaultAdminToolsResource.flushCache(); }); assertEquals(401, exception.getResponse().getStatus()); - verify(logger).warn("Failed to flush the cache due to restricted rights."); + assertEquals("Failed to flush the cache due to restricted rights.", logCapture.getMessage(0)); } @Test @@ -263,15 +257,13 @@ void flushCache() @Test void uploadPackageArchiveNoRights() throws AccessDeniedException { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); doThrow(new AccessDeniedException(Right.ADMIN, user, null)).when(contextualAuthorizationManager) .checkAccess(Right.ADMIN); WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { this.defaultAdminToolsResource.uploadPackageArchive("", ""); }); assertEquals(401, exception.getResponse().getStatus()); - verify(logger).warn("Failed to begin the package upload due to insufficient rights."); + assertEquals("Failed to begin the package upload due to insufficient rights.", logCapture.getMessage(0)); } @Test @@ -300,8 +292,6 @@ void uploadPackageArchiveJobFound() @Test void uploadPackageArchiveError() throws JobException { - when(logger.isWarnEnabled()).thenReturn(true); - ReflectionUtils.setFieldValue(defaultAdminToolsResource, "logger", this.logger); List jobId = List.of("adminTools", "upload", "attachReference", "startTime"); when(jobExecutor.getJob(jobId)).thenReturn(null); when(jobExecutor.execute(UploadJob.JOB_TYPE, new PackageUploadJobRequest("attachReference", jobId))).thenThrow( @@ -311,7 +301,61 @@ void uploadPackageArchiveError() throws JobException defaultAdminToolsResource.uploadPackageArchive("attachReference", "startTime"); }); assertEquals(500, exception.getResponse().getStatus()); - verify(logger).warn("Failed to begin package upload job. Root cause: [{}]", - "JobException: error when executing the job"); + assertEquals("Failed to begin package upload job. Root cause: [JobException: error when executing the job]", + logCapture.getMessage(0)); + } + + @Test + void flushJMXCacheNoRights() throws AccessDeniedException + { + doThrow(new AccessDeniedException(Right.ADMIN, user, null)).when(contextualAuthorizationManager) + .checkAccess(Right.ADMIN); + WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { + this.defaultAdminToolsResource.flushJMXCache(); + }); + assertEquals(401, exception.getResponse().getStatus()); + assertEquals("Failed to flush JMX caches due to restricted rights.", logCapture.getMessage(0)); + } + + @Test + void flushJMXCache() throws JMException, IOException + { + when(cacheDataFlusher.clearAllCache()).thenReturn(true); + assertEquals(200, defaultAdminToolsResource.flushJMXCache().getStatus()); + } + + @Test + void flushJMXCacheErrors() throws JMException, IOException + { + when(cacheDataFlusher.clearAllCache()).thenReturn(false); + assertEquals(200, defaultAdminToolsResource.flushJMXCache().getStatus()); + assertEquals("There were some errors while flushing the JMX cache.", logCapture.getMessage(0)); + } + + @Test + void flushJMXEntryCacheNoRights() throws AccessDeniedException + { + doThrow(new AccessDeniedException(Right.PROGRAM, user, null)).when(contextualAuthorizationManager) + .checkAccess(Right.PROGRAM); + WebApplicationException exception = assertThrows(WebApplicationException.class, () -> { + this.defaultAdminToolsResource.flushJMXEntryCache("test"); + }); + assertEquals(401, exception.getResponse().getStatus()); + assertEquals("Failed to flush JMX cache due to restricted rights.", logCapture.getMessage(0)); + } + + @Test + void flushJMXEntryCache() throws JMException, IOException + { + when(cacheDataFlusher.clearCache("test")).thenReturn(true); + assertEquals(200, defaultAdminToolsResource.flushJMXEntryCache("test").getStatus()); + } + + @Test + void flushJMXEntryCacheNotFound() throws JMException, IOException + { + when(cacheDataFlusher.clearCache("test")).thenReturn(false); + assertEquals(404, defaultAdminToolsResource.flushJMXEntryCache("test").getStatus()); + assertEquals("[test] JMX cache not found.", logCapture.getMessage(0)); } } diff --git a/application-admintools-ui/src/main/resources/AdminTools/CacheDetailedView.xml b/application-admintools-ui/src/main/resources/AdminTools/CacheDetailedView.xml new file mode 100644 index 00000000..6eecdcbb --- /dev/null +++ b/application-admintools-ui/src/main/resources/AdminTools/CacheDetailedView.xml @@ -0,0 +1,77 @@ + + + + + + AdminTools + CacheDetailedView + + + 0 + xwiki:XWiki.Admin + AdminTools.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + Cache detailed view + + false + xwiki/2.1 + true + {{velocity}} +#includeMacros('AdminTools.Code.CacheMacros') +#set ($discard = $xwiki.jsx.use('AdminTools.Code.CacheMacros')) +#set ($cacheName = $request.cacheName) +#if ($cacheName && $cacheName != '') + #set ($details = $services.admintools.getDetailedCacheData($cacheName).entrySet()) + #if ($details) + {{html clean='false'}} + <h2>$cacheName</h2> + <h3>$escapetool.xml($services.localization.render('adminTools.cache.jmx.details.header'))</h3> + <ul> + #foreach($attr in $details) + <li>$attr.getKey() : $attr.getValue()</li> + #end + </ul> + <hr> + <div> + #set ($tableUrl = $xwiki.getURL('AdminTools.JMXManagedCache')) + #set ($flushRef = "${request.getContextPath()}/rest/admintools/flushCache/jmx/cache?cacheName=${cacheName}") + <a class='btn btn-default' href="${tableUrl}"> + $escapetool.xml($services.localization.render('adminTools.cache.jmx.button.return'))</a> + <a class='btn btn-danger cache-flush-action' href="${flushRef}" data-cacheName="${cacheName}"> + $escapetool.xml($services.localization.render('adminTools.cache.jmx.button.flush.cache'))</a> + </div> + #showDeleteEventsModal() + {{/html}} + #else + {{error}} + $escapetool.xml($services.localization.render('adminTools.cache.jmx.details.notFound')) + {{/error}} + #end +#else + {{info}} + $escapetool.xml($services.localization.render('adminTools.cache.jmx.details.noName')) + {{/info}} +#end +{{/velocity}} + + diff --git a/application-admintools-ui/src/main/resources/AdminTools/ChangeDocumentUser.xml b/application-admintools-ui/src/main/resources/AdminTools/ChangeDocumentUser.xml index 059f867b..552f9b6a 100644 --- a/application-admintools-ui/src/main/resources/AdminTools/ChangeDocumentUser.xml +++ b/application-admintools-ui/src/main/resources/AdminTools/ChangeDocumentUser.xml @@ -172,7 +172,7 @@ <input type='hidden' name='confirm' value='true' /> #if ($docCount > 0) <input class='button' type='submit' value="$escapetool.xml($services.localization.render( - 'adminTools.security.changeDocumentUser.button.confirm'))" /> + 'adminTools.button.confirm'))" /> #end <a class='button secondary' href="$doc.getURL()">$escapetool.xml($services.localization.render( 'adminTools.security.changeDocumentUser.button.cancel'))</a> diff --git a/application-admintools-ui/src/main/resources/AdminTools/Code/CacheLivedataJson.xml b/application-admintools-ui/src/main/resources/AdminTools/Code/CacheLivedataJson.xml new file mode 100644 index 00000000..c605540a --- /dev/null +++ b/application-admintools-ui/src/main/resources/AdminTools/Code/CacheLivedataJson.xml @@ -0,0 +1,84 @@ + + + + + + AdminTools.Code + CacheLivedataJson + + + 0 + xwiki:XWiki.Admin + AdminTools.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + CacheLivedataJson + + false + xwiki/2.1 + true + {{include reference="AdminTools.Code.Macros" /}} + +{{velocity}} + #if ($xcontext.action == 'get') + #set ($offset = $numbertool.toNumber($request.offset).intValue()) + ## The offset sent by the live table starts at 1. + #set ($offset = $offset - 1) + #if (!$offset || $offset < 0) + #set ($offset = 0) + #end + #set ($limit = $numbertool.toNumber($request.limit).intValue()) + #if (!$limit) + #set ($limit = 15) + #end + #set ($order = 'desc') + #if ($request.dir) + #set ($order = $request.dir) + #end + #set ($jmxCache = $services.admintools.getJMXCache($request.get('cacheName'), $order)) + #if ($offset < $jmxCache.size()) + #set ($toIndex = $mathtool.min($mathtool.add($offset, $limit), $jmxCache.size())) + #getSubset($jmxCache, $offset, $toIndex, $resultList) + #end + #set ($results = { + "totalrows": $jmxCache.size(), + "returnedrows": $resultList.size(), + "offset": $mathtool.add($offset, 1), + "rows": [] + }) + #foreach ($currentCache in $resultList) + #set ($cacheName = $escapetool.xml($currentCache.getCacheName())) + #set ($docUrl = $xwiki.getURL('AdminTools.CacheDetailedView', 'view', "cacheName=${cacheName}")) + #set ($docName = "<a href='${docUrl}'>${cacheName}</a>") + #set ($flushRef = "${request.getContextPath()}/rest/admintools/flushCache/jmx/cache?cacheName=${cacheName}") + #set ($flush = "<a class='cache-flush-action' href='${flushRef}' data-cacheName='${cacheName}'>" + + "${escapetool.xml($services.localization.render('adminTools.cache.jmx.button.flush.cache'))}</a>") + #set ($discard = $results.rows.add({ + 'cacheName' : $docName, + 'evictionSize' : $currentCache.getFormattedCacheSize(), + 'action' : "${services.icon.renderHTML('rotate-right')} ${flush}" + })) + #end + #jsonResponse($results) + #end +{{/velocity}} + diff --git a/application-admintools-ui/src/main/resources/AdminTools/Code/CacheMacros.xml b/application-admintools-ui/src/main/resources/AdminTools/Code/CacheMacros.xml new file mode 100644 index 00000000..3a8b1392 --- /dev/null +++ b/application-admintools-ui/src/main/resources/AdminTools/Code/CacheMacros.xml @@ -0,0 +1,216 @@ + + + + + + AdminTools.Code + CacheMacros + + + 0 + xwiki:XWiki.Admin + AdminTools.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + CacheMacros + + false + xwiki/2.1 + true + {{velocity output='false'}} +#macro(showDeleteEventsModal) + <div class="modal fade" id="flushCache" tabindex="-1" role="dialog"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">&times;</button> + <h4 class="modal-title"> + $escapetool.xml($services.localization.render('adminTools.cache.jmx.modal.header')) + </h4> + </div> + <div class="modal-body"> + <div></div> + </div> + <div class="modal-footer"> + <input type="button" class="btn btn-danger do-delete" + value="$escapetool.xml($services.localization.render('adminTools.button.confirm'))" data-dismiss="modal"> + <input type="button" class="btn btn-default" + value="$escapetool.xml($services.localization.render('cancel'))" data-dismiss="modal"> + </div> + </div> + </div> + </div> +#end +{{/velocity}} + + AdminTools.Code.CacheMacros + 0 + XWiki.JavaScriptExtension + bea2af44-e0ac-4d51-9e25-492e24ca5b6c + + XWiki.JavaScriptExtension + + + + + + + + + 0 + long + 0 + select + forbidden + 0 + 0 + cache + 5 + Caching policy + 0 + + |, + 1 + 0 + long|short|default|forbid + com.xpn.xwiki.objects.classes.StaticListClass + + + PureText + 0 + PureText + code + 2 + Code + 0 + 20 + 50 + 0 + com.xpn.xwiki.objects.classes.TextAreaClass + + + 0 + name + 1 + Name + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + select + yesno + parse + 4 + Parse content + 0 + com.xpn.xwiki.objects.classes.BooleanClass + + + 0 + 0 + select + forbidden + 0 + 0 + use + 3 + Use this extension + 0 + + |, + 1 + 0 + currentPage|onDemand|always + com.xpn.xwiki.objects.classes.StaticListClass + + + + long + + + define('admin-tools-cache-flush-jmx', { + prefix: 'adminTools.cache.jmx.messages.', + keys: [ + 'all', + 'done', + 'cache', + 'error', + 'notFound' + ] +}); + +require(['jquery', 'xwiki-l10n!admin-tools-cache-flush-jmx'], function($, l10n) { + const redirectToDetailedView = function(cacheName) { + let documentReference = XWiki.Model.resolve('AdminTools.CacheDetailedView', XWiki.EntityType.DOCUMENT); + window.location.href = new XWiki.Document(documentReference).getURL('view', $.param({cacheName: cacheName})); + }; + + const sendFlushRequest = function(relatedTarget) { + const cacheName = $(relatedTarget).data('cachename'); + $.post($(relatedTarget).attr('href')).done(function() { + if (cacheName && cacheName != '') { + redirectToDetailedView(cacheName) + } else { + $("#jmxCacheLivedata").data("liveData").updateEntries(); + new XWiki.widgets.Notification(l10n.get('done'), 'done'); + } + }).fail(function(error) { + if (error.status == 404) { + new XWiki.widgets.Notification(l10n.get('notFound'), 'error'); + } else { + new XWiki.widgets.Notification(l10n.get('error'), 'error'); + } + }); + } + + $(document).on('click', '#flushCache input.btn-danger.do-delete', function() { + var relatedTarget = $('#flushCache').data('relatedTarget'); + sendFlushRequest(relatedTarget); + }); + + $(document).on('click', '.cache-flush-action', function (event) { + event.preventDefault(); + const cacheName = $(this).data('cachename'); + var modalDescription = l10n.get('all') + if (cacheName) { + modalDescription = l10n.get('cache', cacheName) + } + var modal = $(document).find('#flushCache'); + $(modal).find('.modal-body').text(modalDescription); + modal.data('relatedTarget', event.currentTarget); + modal.modal('show'); + }); +}); + + + AdmintoolsCacheJS + + + + + + onDemand + + + diff --git a/application-admintools-ui/src/main/resources/AdminTools/Code/HealthCheckMacros.xml b/application-admintools-ui/src/main/resources/AdminTools/Code/HealthCheckMacros.xml index 255e132b..877b51a3 100644 --- a/application-admintools-ui/src/main/resources/AdminTools/Code/HealthCheckMacros.xml +++ b/application-admintools-ui/src/main/resources/AdminTools/Code/HealthCheckMacros.xml @@ -92,6 +92,9 @@ <li><a href="#confirmCacheFlushModal" data-toggle="modal" data-target="#confirmCacheFlushModal"> $escapetool.xml($services.localization.render('adminTools.dashboard.healthcheck.flushCache'))</a> </li> + <li><a href="$xwiki.getURL('AdminTools.JMXManagedCache')" target="_blank"> + $escapetool.xml($services.localization.render('adminTools.dashboard.healthcheck.cache.jmx'))</a> + </li> </ul> #cacheFlushModal() </div> diff --git a/application-admintools-ui/src/main/resources/AdminTools/Code/Macros.xml b/application-admintools-ui/src/main/resources/AdminTools/Code/Macros.xml index 43434743..ef7edd16 100644 --- a/application-admintools-ui/src/main/resources/AdminTools/Code/Macros.xml +++ b/application-admintools-ui/src/main/resources/AdminTools/Code/Macros.xml @@ -225,7 +225,7 @@ <div class="modal-footer"> <button type="button" class="btn btn-primary" data-action="$request.getContextPath()/rest/admintools/flushCache"> - $escapetool.xml($services.localization.render('adminTools.dashboard.healthcheck.modal.button')) + $escapetool.xml($services.localization.render('adminTools.button.confirm')) </button> <button type="button" class="btn btn-default" data-dismiss="modal"> $escapetool.xml($services.localization.render('cancel'))</button> diff --git a/application-admintools-ui/src/main/resources/AdminTools/Code/Translations.xml b/application-admintools-ui/src/main/resources/AdminTools/Code/Translations.xml index 2eca3270..7c9ac160 100644 --- a/application-admintools-ui/src/main/resources/AdminTools/Code/Translations.xml +++ b/application-admintools-ui/src/main/resources/AdminTools/Code/Translations.xml @@ -48,6 +48,7 @@ AdminTools.Code.ConfigurationClass_xwikiInstallLocation.hint=Path to the XWiki f admin.admintools.description=Configure Admin Tools Application adminTools.extension.name=Admin Tools Application (Pro) adminTools.extension.title=Admin Tools +adminTools.button.confirm=Confirm ## Dashboard sections adminTools.dashboard.toc.title=Table of contents @@ -84,6 +85,7 @@ adminTools.dashboard.healthcheck.description=Check if your instance is configure adminTools.dashboard.healthcheck.flushCache=Flush instance cache adminTools.dashboard.healthcheck.flushCache.error=There has been an error while flushing the instance cache. adminTools.dashboard.healthcheck.flushCache.success=Cache flushed successfully. +adminTools.dashboard.healthcheck.cache.jmx=View JMX managed cache adminTools.dashboard.healthcheck.java.info=Java status OK. adminTools.dashboard.healthcheck.java.warn=Java version could not be found! Please make sure you correctly installed and configured the paths for your Java installation. For further help, consult the help links. adminTools.dashboard.healthcheck.java.error=You are currently using Java version {0} which is not compatible with the XWiki installation version {1}. Please check the help links to view the compatibility. @@ -131,6 +133,23 @@ adminTools.dashboard.healthcheck.message.error=Critical issues were found, pleas adminTools.dashboard.healthcheck.execution.error=An error occurred while running the health check. adminTools.dashboard.healthcheck.operations=Instance operations adminTools.dashboard.healthcheck.linkLabel=help links +adminTools.dashboard.healthcheck.cache.jmx=View JMX cache +adminTools.cache.jmx=View JMX cache +adminTools.cache.jmx.livedata.cacheName=Cache name +adminTools.cache.jmx.livedata.evictionSize=Cache size +adminTools.cache.jmx.livedata.action=Action +adminTools.cache.jmx.button.flush.all=Clear all cache +adminTools.cache.jmx.button.flush.cache=Flush +adminTools.cache.jmx.button.return=Return to table +adminTools.cache.jmx.details.header=Cache details +adminTools.cache.jmx.details.notFound=Could not find details for cache. +adminTools.cache.jmx.details.noName=Cache details will be displayed in this page. +adminTools.cache.jmx.modal.header=Flush cache +adminTools.cache.jmx.messages.all=Are you sure you want to flush the entire cache? +adminTools.cache.jmx.messages.Done=Cache flush completed +adminTools.cache.jmx.messages.cache=Are you sure you want to flush the cache <{0}>? +adminTools.cache.jmx.messages.error=An error occurred while flushing the cache. Please consult the logs! +adminTools.cache.jmx.messages.notFound=Could not find the cache. ##Instance usage adminTools.dashboard.instanceUsage.description=See the stats related to the usage of the instance. @@ -210,7 +229,6 @@ adminTools.security.changeDocumentUser.newUser.placeholder=Select the new user adminTools.security.changeDocumentUser.fields.title=Fields to replace adminTools.security.changeDocumentUser.button.preview=Preview adminTools.security.changeDocumentUser.button.preview.hint=The next step displays the list of documents that will be modified and will ask for confirmation. -adminTools.security.changeDocumentUser.button.confirm=Confirm adminTools.security.changeDocumentUser.button.cancel=Cancel adminTools.security.changeDocumentUser.button.back=Back to main screen adminTools.security.changeDocumentUser.preview.summary=Documents to update ({0} total) from __URLSTART__{1}__URLEND__ to __URLSTART__{2}__URLEND2__ for fields {3}: @@ -349,7 +367,6 @@ adminTools.dashboard.spamPage.modal.header.commentsCount=Comments count adminTools.dashboard.spamPage.modal.header.docName=Page adminTools.dashboard.spamPage.modal.header.wikiName=Wiki name adminTools.dashboard.healthcheck.modal.content=Are you sure you want to flush the instance cache? -adminTools.dashboard.healthcheck.modal.button=Confirm adminTools.dashboard.healthcheck.modal.wikiBins.title=Recycle bins size for all wikis adminTools.dashboard.healthcheck.modal.wikiBins.error=There has been an error while gathering recycle bins size info for all wikis. adminTools.dashboard.healthcheck.modal.wikiBins.header.wikiName=Wiki name diff --git a/application-admintools-ui/src/main/resources/AdminTools/JMXManagedCache.xml b/application-admintools-ui/src/main/resources/AdminTools/JMXManagedCache.xml new file mode 100644 index 00000000..4b7c3c20 --- /dev/null +++ b/application-admintools-ui/src/main/resources/AdminTools/JMXManagedCache.xml @@ -0,0 +1,78 @@ + + + + + + AdminTools + JMXManagedCache + + + 0 + xwiki:XWiki.Admin + WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + JMX managed cache + + false + xwiki/2.1 + true + {{velocity}} +#includeMacros('AdminTools.Code.CacheMacros') +#set ($discard = $xwiki.jsx.use('AdminTools.Code.CacheMacros')) + +{{html clean='false' wiki='true'}} + <div> + <a class='btn btn-danger cache-flush-action' + href="${request.getContextPath()}/rest/admintools/flushCache/jmx"> + $escapetool.xml($services.localization.render('adminTools.cache.jmx.button.flush.all'))</a> + </div> + <hr> + + #set ($sourceParameters = $escapetool.url({ + 'resultPage': 'AdminTools.Code.CacheLivedataJson', + 'translationPrefix': 'adminTools.cache.jmx.livedata.' + })) + #set ($liveDataConfig= { + 'meta': { + 'propertyDescriptors': [ + {'id': 'cacheName', 'displayer': 'html', 'sortable': false}, + {'id': 'evictionSize', 'displayer': 'text', 'filterable': false}, + {'id': 'action', 'displayer': 'html', 'filterable': false, 'sortable': false} + ], + 'entryDescriptor': { + 'idProperty': 'cacheName' + } + } + }) + {{liveData + id='jmxCacheLivedata' + properties="cacheName, evictionSize, action" + source='liveTable' + sourceParameters="$sourceParameters" + sort='evictionSize:desc' + }}$jsontool.serialize($liveDataConfig){{/liveData}} +{{/html}} + +{{html clean="false"}}#showDeleteEventsModal(){{/html}} +{{/velocity}} +