/** * Copyright (C) 2009 eXo Platform SAS. * * 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 org.exoplatform.portal.resource; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.ServletContext; import org.exoplatform.commons.cache.future.FutureMap; import org.exoplatform.commons.cache.future.Loader; import org.exoplatform.commons.utils.BinaryOutput; import org.exoplatform.commons.utils.ByteArrayOutput; import org.exoplatform.commons.utils.PropertyManager; import org.exoplatform.commons.utils.Safe; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.management.annotations.Impact; import org.exoplatform.management.annotations.ImpactType; import org.exoplatform.management.annotations.Managed; import org.exoplatform.management.annotations.ManagedDescription; import org.exoplatform.management.annotations.ManagedName; import org.exoplatform.management.jmx.annotations.NameTemplate; import org.exoplatform.management.jmx.annotations.Property; import org.exoplatform.management.rest.annotations.RESTEndpoint; import org.exoplatform.portal.resource.compressor.ResourceCompressor; import org.exoplatform.portal.resource.compressor.ResourceType; import org.exoplatform.portal.resource.config.tasks.SkinConfigTask; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.resources.Orientation; import org.exoplatform.web.ControllerContext; import org.exoplatform.web.WebAppController; import org.exoplatform.web.controller.QualifiedName; import org.exoplatform.web.controller.router.URIWriter; import org.exoplatform.web.url.MimeType; import org.gatein.portal.controller.resource.ResourceRequestHandler; @Managed @NameTemplate({ @Property(key = "view", value = "portal"), @Property(key = "service", value = "management"), @Property(key = "type", value = "skin") }) @ManagedDescription("Skin service") @RESTEndpoint(path = "skinservice") public class SkinService extends AbstractResourceService { protected static Log log = ExoLogger.getLogger("portal.SkinService"); private static final String LEFT_P = "\\("; private static final String RIGHT_P = "\\)"; /** Immutable and therefore thread safe. */ private static final Pattern IMPORT_PATTERN = Pattern.compile("(@import\\s+" + "url" + LEFT_P + "['\"]?" + ")([^'\";]+.css)(" + "['\"]?" + RIGHT_P + "\\s*;)"); /** Immutable and therefore thread safe. */ private static final Pattern BACKGROUND_PATTERN = Pattern.compile("(background[^;])+([^;]*;)"); private static final Pattern FONT_FACE_PATTERN = Pattern.compile("(src[^;])+([^;]*;)"); private static final Pattern URL_PATTERN = Pattern.compile("(url" + LEFT_P + "['\"]?)([^'\";" + RIGHT_P + "]+)(['\"]?\\))"); private static final Pattern DOLLAR_PATTERN = Pattern.compile("\\$"); /** Immutable and therefore thread safe. */ private static final Pattern LT = Pattern.compile("[^{;]*;\\s*/\\*\\s*orientation=lt\\s*\\*/"); /** Immutable and therefore thread safe. */ private static final Pattern RT = Pattern.compile("[^{;]*;\\s*/\\*\\s*orientation=rt\\s*\\*/"); public static final String DEFAULT_SKIN = "Default"; private final Map<SkinKey, SkinConfig> portalSkins_; private final Map<SkinKey, SkinConfig> skinConfigs_; private final HashSet<String> availableSkins_; private final FutureMap<String, CachedStylesheet, SkinContext> ltCache; private final FutureMap<String, CachedStylesheet, SkinContext> rtCache; private final Map<String, Set<String>> portletThemes_; /** * The name of the portal container */ final String portalContainerName; /** * An id used for caching request. The id life cycle is the same than the class instance because we consider css will change * until server is restarted. Of course this only applies for the developing mode set to false. */ final String id = Long.toString(System.currentTimeMillis()); private static final long MAX_AGE; static { long seconds = 86400; String propValue = PropertyManager.getProperty("gatein.assets.css.max-age"); if (propValue != null) { try { seconds = Long.valueOf(propValue); } catch (NumberFormatException e) { log.warn("The gatein.assets.css.max-age property is not set properly."); } } MAX_AGE = seconds; } static class SkinContext { final ControllerContext controller; final Orientation orientation; SkinContext(ControllerContext controller, Orientation orientation) { this.controller = controller; this.orientation = orientation; } } /** * Returns {@code true} if the given {@code url} starts with "//" or "http:" or "https:". * Otherwise, returns {@code false}. * * @param url the URL to decide about * @return see above */ static boolean isExternalUrl(String url) { return url != null && (url.startsWith("//") || url.startsWith("http:") || url.startsWith("https:")); } public SkinService(ExoContainerContext context, ResourceCompressor compressor) { super(compressor); Loader<String, CachedStylesheet, SkinContext> loader = new Loader<String, CachedStylesheet, SkinContext>() { public CachedStylesheet retrieve(SkinContext context, String key) throws Exception { Resource skin = getCSSResource(key, key); if (skin == null) { return null; } StringBuffer sb = new StringBuffer(); processCSSRecursively(context.controller, sb, true, skin, context.orientation); String css = sb.toString(); try { if (SkinService.this.compressor.isSupported(ResourceType.STYLESHEET)) { css = SkinService.this.compressor.compress(css, ResourceType.STYLESHEET); } } catch (Exception e) { log.warn("Error when compressing CSS " + key + " for orientation " + context + " will use normal CSS instead", e); } return new CachedStylesheet(css); } }; // portalSkins_ = new LinkedHashMap<SkinKey, SkinConfig>(); skinConfigs_ = new LinkedHashMap<SkinKey, SkinConfig>(20); availableSkins_ = new HashSet<String>(5); ltCache = new FutureMap<String, CachedStylesheet, SkinContext>(loader); rtCache = new FutureMap<String, CachedStylesheet, SkinContext>(loader); portletThemes_ = new HashMap<String, Set<String>>(); portalContainerName = context.getPortalContainerName(); addResourceResolver(new CompositeResourceResolver(portalContainerName, skinConfigs_)); } /** * Add a new category for portlet themes if it does not exist * * @param categoryName the category name */ public void addCategoryTheme(String categoryName) { if (!portletThemes_.containsKey(categoryName)) { portletThemes_.put(categoryName, new HashSet<String>()); } } /** * @deprecated use {@link #addPortalSkin(String, String, String)} instead */ @Deprecated public void addPortalSkin(String module, String skinName, String cssPath, ServletContext scontext) { addPortalSkin(module, skinName, cssPath); } /** * Add a portal skin with the <code>priority</code> is Integer.MAX_VALUE and the <code>overwrite</code> is false by default * * @param module * @param skinName * @param cssPath */ public void addPortalSkin(String module, String skinName, String cssPath) { addPortalSkin(module, skinName, cssPath, Integer.MAX_VALUE, false); } /** * @deprecated use {@link #addPortalSkin(String, String, String, boolean)} instead */ @Deprecated public void addPortalSkin(String module, String skinName, String cssPath, ServletContext scontext, boolean overwrite) { addPortalSkin(module, skinName, cssPath, overwrite); } /** * Add a portal skin with the <code>priority</code> is Integer.MAX_VALUE by default * * @param module * @param skinName * @param cssPath * @param overwrite */ public void addPortalSkin(String module, String skinName, String cssPath, boolean overwrite) { addPortalSkin(module, skinName, cssPath, Integer.MAX_VALUE, overwrite); } /** * Register a portal skin * * @param module skin module identifier * @param skinName skin name * @param cssPath path uri to the css file. This is relative to the root context, use leading '/' * @param overwrite if any previous skin should be replaced by that one * @param cssPriority priority to support sorting in skin list */ public void addPortalSkin(String module, String skinName, String cssPath, int priority, boolean overwrite) { availableSkins_.add(skinName); SkinKey key = new SkinKey(module, skinName); SkinConfig skinConfig = portalSkins_.get(key); if (skinConfig == null || overwrite) { if (priority < 0) { priority = Integer.MAX_VALUE; } skinConfig = new SimpleSkin(this, module, skinName, cssPath, priority); portalSkins_.put(key, skinConfig); if (log.isDebugEnabled()) { log.debug("Adding Portal skin : Bind " + key + " to " + skinConfig); } } } /** * Register a portal skin with the specific <code>cssData</code> * * @deprecated This method is not supported anymore. The resource resolver pluggability mechanism should be used somehow * @param module skin module identifier * @param skinName skin name * @param cssPath path uri to the css file. This is relative to the root context, use leading '/' * @param cssData the content of css */ @Deprecated public void addPortalSkin(String module, String skinName, String cssPath, String cssData) { throw new UnsupportedOperationException("This method is not supported anymore."); } /** * @deprecated use {@link #addSkin(String, String, String)} instead */ @Deprecated public void addSkin(String module, String skinName, String cssPath, ServletContext scontext) { addSkin(module, skinName, cssPath); } /** * Add a skin with the <code>priority</code> is Integer.MAX_VALUE and the <code>overwrite</code> is false by default * * @param module * @param skinName * @param cssPath */ public void addSkin(String module, String skinName, String cssPath) { addSkin(module, skinName, cssPath, Integer.MAX_VALUE, false); } /** * @deprecated use {@link #addSkin(String, String, String, boolean)} instead */ @Deprecated public void addSkin(String module, String skinName, String cssPath, ServletContext scontext, boolean overwrite) { addSkin(module, skinName, cssPath, overwrite); } /** * Add a portal skin with the <code>priority</code> is Integer.MAX_VALUE * * @param module * @param skinName * @param cssPath * @param overwrite */ public void addSkin(String module, String skinName, String cssPath, boolean overwrite) { addSkin(module, skinName, cssPath, Integer.MAX_VALUE, overwrite); } /** * * Register the Skin for available portal Skins. Support priority * * @param module skin module identifier * @param skinName skin name * @param cssPath path uri to the css file. This is relative to the root context, use leading '/' * @param overwrite if any previous skin should be replaced by that one * @param priority priority to support sorting in skin list */ public void addSkin(String module, String skinName, String cssPath, int priority, boolean overwrite) { availableSkins_.add(skinName); SkinKey key = new SkinKey(module, skinName); SkinConfig skinConfig = skinConfigs_.get(key); if (skinConfig == null || overwrite) { if (priority < 0) { priority = Integer.MAX_VALUE; } skinConfig = new SimpleSkin(this, module, skinName, cssPath, priority); skinConfigs_.put(key, skinConfig); if (log.isDebugEnabled()) { log.debug("Adding skin : Bind " + key + " to " + skinConfig); } } } /** * * Register the Skin for available portal Skins. Do not replace existed Skin * * @param module skin module identifier * @param skinName skin name * @param cssPath path uri to the css file. This is relative to the root context, use leading '/' * @param scontext the webapp's {@link javax.servlet.ServletContext} */ @Deprecated public void addSkin(String module, String skinName, String cssPath, String cssData) { availableSkins_.add(skinName); SkinKey key = new SkinKey(module, skinName); SkinConfig skinConfig = skinConfigs_.get(key); if (skinConfig == null) { skinConfigs_.put(key, new SimpleSkin(this, module, skinName, cssPath)); } ltCache.remove(cssPath); rtCache.remove(cssPath); } /** * @param skinTasks */ public void addSkins(List<SkinConfigTask> skinTasks, ServletContext scontext) { for (SkinConfigTask task : skinTasks) { task.execute(this, scontext); } } /** * Merge several skins into one single skin. * * @param skins the skins to merge * @return the merged skin */ public Skin merge(Collection<SkinConfig> skins) { return new CompositeSkin(this, skins); } /** * Registry theme category with its themes for portlet Theme * * @param categoryName category name that will be registried * @param themesName list theme name of categoryName */ public void addTheme(String categoryName, List<String> themesName) { if (!portletThemes_.containsKey(categoryName)) portletThemes_.put(categoryName, new HashSet<String>()); Set<String> catThemes = portletThemes_.get(categoryName); for (String theme : themesName) catThemes.add(theme); } /** * Get names of all the currently registered skins. * * @return an unmodifiable Set of the currently registered skins */ public Set<String> getAvailableSkinNames() { return availableSkins_; } /** * Return the CSS content of the file specified by the given URI. * * * * @param context * @param compress * @return the css contet or null if not found. */ public String getCSS(ControllerContext context, boolean compress) { try { final ByteArrayOutput output = new ByteArrayOutput(); boolean success = renderCSS(context, new ResourceRenderer() { public BinaryOutput getOutput() { return output; } public void setExpiration(long seconds) { } }, compress); if (success) { return output.getString(); } else { return null; } } catch (IOException e) { // log.error("Error while rendering css " + path, e); return null; } catch (RenderingException e) { // log.error("Error while rendering css " + path, e); return null; } } /** * Render css content of the file specified by the given URI * * * @param context * @param renderer the webapp's {@link org.exoplatform.portal.resource.ResourceRenderer} * @param path path uri to the css file * @param compress * @throws RenderingException * @throws IOException * @return <code>true</code> if the <code>CSS resource </code>is found and rendered; <code>false</code> otherwise. */ public boolean renderCSS(ControllerContext context, ResourceRenderer renderer, boolean compress) throws RenderingException, IOException { String dir = context.getParameter(ResourceRequestHandler.ORIENTATION_QN); Orientation orientation = "rt".equals(dir) ? Orientation.RT : Orientation.LT; // Check if it is running under developing mode String resource = "/" + context.getParameter(ResourceRequestHandler.RESOURCE_QN) + ".css"; if (!compress) { StringBuffer sb = new StringBuffer(); Resource skin = getCSSResource(resource, resource); if (skin != null) { processCSSRecursively(context, sb, false, skin, orientation); byte[] bytes = sb.toString().getBytes("UTF-8"); renderer.getOutput().write(bytes); return true; } } else { FutureMap<String, CachedStylesheet, SkinContext> cache = orientation == Orientation.LT ? ltCache : rtCache; CachedStylesheet cachedCss = cache.get(new SkinContext(context, orientation), resource); if (cachedCss != null) { renderer.setExpiration(MAX_AGE); cachedCss.writeTo(renderer.getOutput()); return true; } } // return false; } /** * Return CSS data corresponding to the <code>path</code> * * * @param context * @param path path uri to the css file * @return css content of URI file or null if not found */ @Deprecated public String getMergedCSS(ControllerContext context, String path) { return getCSS(context, true); } /** * Return a collection of Portal Skins that its elements are ordered by CSS priority * * @param skinName name of Portal Skin * @return all org.exoplatform.portal.resource.SkinConfig of Portal Skin */ public Collection<SkinConfig> getPortalSkins(String skinName) { Set<SkinKey> keys = portalSkins_.keySet(); List<SkinConfig> portalSkins = new ArrayList<SkinConfig>(); for (SkinKey key : keys) { if (key.getName().equals(skinName)) portalSkins.add(portalSkins_.get(key)); } Collections.sort(portalSkins, new Comparator<SkinConfig>() { public int compare(SkinConfig o1, SkinConfig o2) { return o1.getCSSPriority() - o2.getCSSPriority(); } }); return portalSkins; } /** * Return a collection of SkinConfig based on SkinVisitor provided as the argument * * @param visitor * @return */ public Collection<SkinConfig> findSkins(SkinVisitor visitor) { Set<Entry<SkinKey,SkinConfig>> entrySet = portalSkins_.entrySet(); for (Entry<SkinKey,SkinConfig> entry : entrySet) { visitor.visitPortalSkin(entry); } entrySet = skinConfigs_.entrySet(); for (Entry<SkinKey,SkinConfig> entry : entrySet) { visitor.visitSkin(entry); } return visitor.getSkins(); } /** * Return the map of portlet themes * * @return the map of portlet themes */ public Map<String, Set<String>> getPortletThemes() { return portletThemes_; } /** * Return a SkinConfig mapping by the module and skin name * * @param module * @param skinName * @return SkinConfig by SkinKey(module, skinName), or SkinConfig by SkinKey(module, SkinService.DEFAULT_SKIN) */ public SkinConfig getSkin(String module, String skinName) { SkinConfig config = skinConfigs_.get(new SkinKey(module, skinName)); if (config == null) { config = skinConfigs_.get(new SkinKey(module, SkinService.DEFAULT_SKIN)); } return config; } /** * Remove SkinKey from SkinCache by portalName and skinName * * @deprecated the method name is wrong to the behaviour it does. Use {@link #removeSkin(String, String)} instead * * @param portalName * @param skinName */ @Deprecated public void invalidatePortalSkinCache(String portalName, String skinName) { SkinKey key = new SkinKey(portalName, skinName); skinConfigs_.remove(key); } /** * Invalidate skin from the cache * * @param path the key */ public void invalidateCachedSkin(String path) { ltCache.remove(path); rtCache.remove(path); } /** * Returns last modified date of cached css. * <p> * In development mode, it always returns {@link Long#MAX_VALUE}. Return null if cached css can not be found * * @param context * @param path - path must not be null */ public long getLastModified(ControllerContext context) { String resource = "/" + context.getParameter(ResourceRequestHandler.RESOURCE_QN) + ".css"; if (PropertyManager.isDevelopping()) { return Long.MAX_VALUE; } FutureMap<String, CachedStylesheet, SkinContext> cache = ltCache; Orientation orientation = Orientation.LT; String dir = context.getParameter(ResourceRequestHandler.ORIENTATION_QN); if ("rt".equals(dir)) { orientation = Orientation.RT; cache = rtCache; } CachedStylesheet cachedCSS = cache.get(new SkinContext(context, orientation), resource); if (cachedCSS == null) { return Long.MAX_VALUE; } else { return cachedCSS.getLastModified(); } } /** * @deprecated The method name is not clear. Using {@link #removeSkin(String, String)} instead */ @Deprecated public void remove(String module, String skinName) { removeSkin(module, skinName); } /** * Remove a Skin from the service as well as its cache * * @param module * @param skinName */ public void removeSkin(String module, String skinName) { SkinKey key; if (skinName.length() == 0) { key = new SkinKey(module, DEFAULT_SKIN); } else { key = new SkinKey(module, skinName); } removeSkin(key); } /** * Remove a Skin mapped to the <code>key</code> * * @param key key whose mapping skin is to be removed from the service */ public void removeSkin(SkinKey key) { if (key == null) { return; } SkinConfig remove = skinConfigs_.remove(key); if (remove != null) { invalidateCachedSkin(remove.getCSSPath()); } } /** * @deprecated This is deprecated as its name was not clear. Use {@link #removeSkins(List)} instead */ @Deprecated public void remove(List<SkinKey> keys) throws Exception { removeSkins(keys); } /** * Remove SkinConfig from Portal Skin Config by SkinKey * * @param keys SkinKey list these will be removed * @throws Exception */ public void removeSkins(List<SkinKey> keys) { if (keys == null) { return; } for (SkinKey key : keys) { removeSkin(key); } } /** * Remove Skin from Portal available Skin by skin name * * @param skinName name of skin that will be removed * @throws Exception */ public void removeSupportedSkin(String skinName) { availableSkins_.remove(skinName); } /** * Return the number of skin config maintaining in this SkinService * * @return the number of skin config maintaining in this SkinService */ public int size() { return skinConfigs_.size(); } /** * * This method delegates the resource resolving to MainResourceResolver and prints out appropriated log messages * * Consider the two cases the method is invoked * * Case 1: Resolve nested .css file * * In Stylesheet.css we have the statement * * @import url(xyzt.css); * * To resolve the resource from xyzt.css, getCSSResource("xyzt.css", "Stylesheet.css") is called * * Case 2: Resolve top root .css file * * To resolve a top root Stylesheet.css file, getCSSResource("Stylesheet.css", "Stylesheet.css") is called * * @param cssPath * @param outerCssFile * @return * */ private Resource getCSSResource(String cssPath, String outerCssFile) { Resource resource = mainResolver.resolve(cssPath); if (resource == null && log.isErrorEnabled()) { String logMessage; if (!cssPath.equals(outerCssFile)) { int lastIndexOfSlash = cssPath.lastIndexOf('/'); String loadedCssFile = (lastIndexOfSlash >= 0) ? (cssPath.substring(lastIndexOfSlash + 1)) : cssPath; logMessage = "Invalid <CSS FILE> configuration, please check the @import url(" + loadedCssFile + ") in " + outerCssFile + " , SkinService could not load the skin " + cssPath; } else { logMessage = "Not found <CSS FILE>, the path " + cssPath + " is invalid, SkinService could not load the skin " + cssPath; } log.error(logMessage); } return resource; } /** * Apply CSS for Skin <br/> * If skin is null, do nothing * * @param context * @param appendable * @param merge * @param skin * @param orientation * @throws RenderingException * @throws IOException */ private void processCSSRecursively(ControllerContext context, Appendable appendable, boolean merge, Resource skin, Orientation orientation) throws RenderingException, IOException { if (skin == null) { return; } // The root URL for the entry String basePath = skin.getContextPath() + skin.getParentPath(); // Reader tmp = skin.read(); if (tmp == null) { throw new RenderingException("No skin resolved for path " + skin.getResourcePath()); } BufferedReader reader = new SkipCommentReader(tmp, new CommentBlockHandler.OrientationCommentBlockHandler()); try { String line = reader.readLine(); while (line != null) { line = proccessOrientation(line, orientation); line = processURL(BACKGROUND_PATTERN, line, basePath); line = processURL(FONT_FACE_PATTERN, line, basePath); Matcher matcher = IMPORT_PATTERN.matcher(line); while (matcher.find()) { String includedPath = matcher.group(2); String str = null; if (isExternalUrl(includedPath)) { /* leave an external URL as it is */ str = matcher.group(); } else { /* an internal path */ if (!includedPath.startsWith("/")) { includedPath = basePath + includedPath; } StringBuffer strReplace = new StringBuffer(); if (merge) { Resource ssskin = getCSSResource(includedPath, basePath + skin.getFileName()); processCSSRecursively(context, strReplace, merge, ssskin, orientation); } else { // Remove leading '/' and trailing '.css' String resource = includedPath.substring(1, includedPath.length() - ".css".length()); // Map<QualifiedName, String> params = new HashMap<QualifiedName, String>(); params.put(ResourceRequestHandler.VERSION_QN, ResourceRequestHandler.VERSION); params.put(ResourceRequestHandler.ORIENTATION_QN, orientation == Orientation.RT ? "rt" : "lt"); params.put(ResourceRequestHandler.COMPRESS_QN, merge ? "min" : ""); params.put(WebAppController.HANDLER_PARAM, "skin"); params.put(ResourceRequestHandler.RESOURCE_QN, resource); StringBuilder embeddedPath = new StringBuilder(); context.renderURL(params, new URIWriter(embeddedPath, MimeType.PLAIN)); // strReplace.append(matcher.group(1)); strReplace.append(embeddedPath); strReplace.append(matcher.group(3)); } str = DOLLAR_PATTERN.matcher(strReplace.toString()).replaceAll("\\\\\\$"); } matcher.appendReplacement((StringBuffer) appendable, str); } matcher.appendTail((StringBuffer) appendable); if ((line = reader.readLine()) != null) { appendable.append("\n"); } } } finally { Safe.close(reader); } } private String processURL(Pattern pattern, String line, String basePath) { Matcher patternMatcher = pattern.matcher(line); StringBuffer tmpBuilder = new StringBuffer(); while (patternMatcher.find()) { Matcher urlMatcher = URL_PATTERN.matcher(patternMatcher.group()); StringBuffer tmpURL = new StringBuffer(); while(urlMatcher.find()) { if (!urlMatcher.group(2).startsWith("\"/") && !urlMatcher.group(2).startsWith("'/") && !urlMatcher.group(2).startsWith("/") ) { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(urlMatcher.group(1)); urlBuilder.append(basePath); urlBuilder.append(urlMatcher.group(2)); urlBuilder.append(urlMatcher.group(3)); urlMatcher.appendReplacement(tmpURL, urlBuilder.toString()); } } urlMatcher.appendTail(tmpURL); patternMatcher.appendReplacement(tmpBuilder, tmpURL.toString()); } patternMatcher.appendTail(tmpBuilder); return tmpBuilder.toString(); } private String proccessOrientation(String line, Orientation orientation) { Pattern orientationPattern = orientation == Orientation.LT ? RT : LT; Matcher matcher = orientationPattern.matcher(line); StringBuffer tmpBuilder = new StringBuffer(); while (matcher.find()) { matcher.appendReplacement(tmpBuilder, ""); } matcher.appendTail(tmpBuilder); return tmpBuilder.toString(); } /** * Get all available skin * * @return all available skin * */ @Managed @ManagedDescription("The list of registered skins identifiers") public String[] getSkinList() { // get all available skin List<String> availableSkin = new ArrayList<String>(); for (String skin : availableSkins_) { availableSkin.add(skin); } // sort skin name asc Collections.sort(availableSkin); return availableSkin.toArray(new String[availableSkin.size()]); } /** * Clean cache, reload all Skins */ @Managed @ManagedDescription("Reload all skins") @Impact(ImpactType.WRITE) public void reloadSkins() { // remove all ltCache, rtCache ltCache.clear(); rtCache.clear(); } /** * reload skin by skin ID * * @param skinId the skin ID that will be reloaded */ @Managed @ManagedDescription("Reload a specified skin") public void reloadSkin(@ManagedDescription("The skin id") @ManagedName("skinId") String skinId) { ltCache.remove(skinId); rtCache.remove(skinId); } }