Merge pull request #568 from stianst/master

KEYCLOAK-562 Cache theme instances
This commit is contained in:
Stian Thorgersen 2014-07-30 16:10:41 +01:00
commit ba8fe1ddaf
19 changed files with 184 additions and 49 deletions

View file

@ -13,10 +13,10 @@ import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean; import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean; import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.audit.Event; import org.keycloak.audit.Event;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -73,10 +73,10 @@ public class FreeMarkerAccountProvider implements AccountProvider {
public Response createResponse(AccountPages page) { public Response createResponse(AccountPages page) {
Map<String, Object> attributes = new HashMap<String, Object>(); Map<String, Object> attributes = new HashMap<String, Object>();
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme; Theme theme;
try { try {
theme = themeManager.createTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT); theme = themeProvider.getTheme(realm.getAccountTheme(), Theme.Type.ACCOUNT);
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to create theme", e); logger.error("Failed to create theme", e);
return Response.serverError().build(); return Response.serverError().build();

View file

@ -14,36 +14,49 @@ import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class ExtendingThemeManager implements ThemeProvider { public class ExtendingThemeManager implements ThemeProvider {
private final KeycloakSession session;
private final ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache;
private List<ThemeProvider> providers; private List<ThemeProvider> providers;
private String defaultTheme; private String defaultTheme;
private int staticMaxAge; private int staticMaxAge;
public ExtendingThemeManager(KeycloakSession session) { public ExtendingThemeManager(KeycloakSession session, ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache) {
providers = new LinkedList(); this.session = session;
this.themeCache = themeCache;
for (ThemeProvider p : session.getAllProviders(ThemeProvider.class)) {
if (!p.getClass().equals(ExtendingThemeManager.class)) {
providers.add(p);
}
}
Collections.sort(providers, new Comparator<ThemeProvider>() {
@Override
public int compare(ThemeProvider o1, ThemeProvider o2) {
return o2.getProviderPriority() - o1.getProviderPriority();
}
});
this.defaultTheme = Config.scope("theme").get("default", "keycloak"); this.defaultTheme = Config.scope("theme").get("default", "keycloak");
this.staticMaxAge = Config.scope("theme").getInt("staticMaxAge", -1); this.staticMaxAge = Config.scope("theme").getInt("staticMaxAge", -1);
} }
private List<ThemeProvider> getProviders() {
if (providers == null) {
providers = new LinkedList();
for (ThemeProvider p : session.getAllProviders(ThemeProvider.class)) {
if (!(p instanceof ExtendingThemeManager)) {
if (!p.getClass().equals(ExtendingThemeManager.class)) {
providers.add(p);
}
}
}
Collections.sort(providers, new Comparator<ThemeProvider>() {
@Override
public int compare(ThemeProvider o1, ThemeProvider o2) {
return o2.getProviderPriority() - o1.getProviderPriority();
}
});
}
return providers;
}
public int getStaticMaxAge() { public int getStaticMaxAge() {
return staticMaxAge; return staticMaxAge;
} }
@ -54,11 +67,27 @@ public class ExtendingThemeManager implements ThemeProvider {
} }
@Override @Override
public Theme createTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) throws IOException {
if (name == null) { if (name == null) {
name = defaultTheme; name = defaultTheme;
} }
if (themeCache != null) {
ExtendingThemeManagerFactory.ThemeKey key = ExtendingThemeManagerFactory.ThemeKey.get(name, type);
Theme theme = themeCache.get(key);
if (theme == null) {
theme = loadTheme(name, type);
if (themeCache.putIfAbsent(key, theme) != null) {
theme = themeCache.get(key);
}
}
return theme;
} else {
return loadTheme(name, type);
}
}
private Theme loadTheme(String name, Theme.Type type) throws IOException {
Theme theme = findTheme(name, type); Theme theme = findTheme(name, type);
if (theme.getParentName() != null) { if (theme.getParentName() != null) {
List<Theme> themes = new LinkedList<Theme>(); List<Theme> themes = new LinkedList<Theme>();
@ -88,7 +117,7 @@ public class ExtendingThemeManager implements ThemeProvider {
@Override @Override
public Set<String> nameSet(Theme.Type type) { public Set<String> nameSet(Theme.Type type) {
Set<String> themes = new HashSet<String>(); Set<String> themes = new HashSet<String>();
for (ThemeProvider p : providers) { for (ThemeProvider p : getProviders()) {
themes.addAll(p.nameSet(type)); themes.addAll(p.nameSet(type));
} }
return themes; return themes;
@ -96,7 +125,7 @@ public class ExtendingThemeManager implements ThemeProvider {
@Override @Override
public boolean hasTheme(String name, Theme.Type type) { public boolean hasTheme(String name, Theme.Type type) {
for (ThemeProvider p : providers) { for (ThemeProvider p : getProviders()) {
if (p.hasTheme(name, type)) { if (p.hasTheme(name, type)) {
return true; return true;
} }
@ -110,10 +139,10 @@ public class ExtendingThemeManager implements ThemeProvider {
} }
private Theme findTheme(String name, Theme.Type type) { private Theme findTheme(String name, Theme.Type type) {
for (ThemeProvider p : providers) { for (ThemeProvider p : getProviders()) {
if (p.hasTheme(name, type)) { if (p.hasTheme(name, type)) {
try { try {
return p.createTheme(name, type); return p.getTheme(name, type);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to create " + type.toString().toLowerCase() + " theme", e); throw new RuntimeException("Failed to create " + type.toString().toLowerCase() + " theme", e);
} }

View file

@ -0,0 +1,90 @@
package org.keycloak.freemarker;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ExtendingThemeManagerFactory implements ThemeProviderFactory {
private ConcurrentHashMap<ThemeKey, Theme> themeCache = new ConcurrentHashMap<ThemeKey, Theme>();
private ExtendingThemeManager themeManager;
@Override
public ThemeProvider create(KeycloakSession session) {
return new ExtendingThemeManager(session, themeCache);
}
@Override
public void init(Config.Scope config) {
if(Config.scope("theme").getBoolean("cacheThemes", true)) {
themeCache = new ConcurrentHashMap<ThemeKey, Theme>();
}
}
@Override
public void close() {
}
@Override
public String getId() {
return "extending";
}
public static class ThemeKey {
private String name;
private Theme.Type type;
public static ThemeKey get(String name, Theme.Type type) {
return new ThemeKey(name, type);
}
private ThemeKey(String name, Theme.Type type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Theme.Type getType() {
return type;
}
public void setType(Theme.Type type) {
this.type = type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThemeKey themeKey = (ThemeKey) o;
if (name != null ? !name.equals(themeKey.name) : themeKey.name != null) return false;
if (type != themeKey.type) return false;
return true;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (type != null ? type.hashCode() : 0);
return result;
}
}
}

View file

@ -12,17 +12,18 @@ import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class FreeMarkerUtil { public class FreeMarkerUtil {
private Map<String, Template> cache; private ConcurrentHashMap<String, Template> cache;
public FreeMarkerUtil() { public FreeMarkerUtil() {
if (Config.scope("theme").getBoolean("cacheTemplates", false)) { if (Config.scope("theme").getBoolean("cacheTemplates", true)) {
cache = Collections.synchronizedMap(new HashMap<String, Template>()); cache = new ConcurrentHashMap<String, Template>();
} }
} }
@ -34,7 +35,9 @@ public class FreeMarkerUtil {
template = cache.get(key); template = cache.get(key);
if (template == null) { if (template == null) {
template = getTemplate(templateName, theme); template = getTemplate(templateName, theme);
cache.put(key, template); if (cache.putIfAbsent(key, template) != null) {
template = cache.get(key);
}
} }
} else { } else {
template = getTemplate(templateName, theme); template = getTemplate(templateName, theme);

View file

@ -12,7 +12,7 @@ public interface ThemeProvider extends Provider {
public int getProviderPriority(); public int getProviderPriority();
public Theme createTheme(String name, Theme.Type type) throws IOException; public Theme getTheme(String name, Theme.Type type) throws IOException;
public Set<String> nameSet(Theme.Type type); public Set<String> nameSet(Theme.Type type);

View file

@ -0,0 +1 @@
org.keycloak.freemarker.ExtendingThemeManagerFactory

View file

@ -37,7 +37,7 @@ public class DefaultKeycloakThemeProvider implements ThemeProvider {
} }
@Override @Override
public Theme createTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) { if (hasTheme(name, type)) {
return new ClassLoaderTheme(name, type, getClass().getClassLoader()); return new ClassLoaderTheme(name, type, getClass().getClassLoader());
} else { } else {

View file

@ -28,7 +28,7 @@ public class FolderThemeProvider implements ThemeProvider {
} }
@Override @Override
public Theme createTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) { if (hasTheme(name, type)) {
return new FolderTheme(new File(getTypeDir(type), name), type); return new FolderTheme(new File(getTypeDir(type), name), type);
} }

View file

@ -5,9 +5,9 @@ import org.keycloak.audit.Event;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.email.freemarker.beans.EventBean; import org.keycloak.email.freemarker.beans.EventBean;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -79,8 +79,8 @@ public class FreeMarkerEmailProvider implements EmailProvider {
private void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException { private void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
try { try {
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeManager.createTheme(realm.getEmailTheme(), Theme.Type.EMAIL); Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
String subject = theme.getMessages().getProperty(subjectKey); String subject = theme.getMessages().getProperty(subjectKey);
String body = freeMarker.processTemplate(attributes, template, theme); String body = freeMarker.processTemplate(attributes, template, theme);

View file

@ -4,10 +4,10 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.FreeMarkerException; import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil; import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.login.LoginFormsPages; import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.freemarker.model.CodeBean; import org.keycloak.login.freemarker.model.CodeBean;
@ -150,10 +150,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Map<String, Object> attributes = new HashMap<String, Object>(); Map<String, Object> attributes = new HashMap<String, Object>();
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme; Theme theme;
try { try {
theme = themeManager.createTheme(realm.getLoginTheme(), Theme.Type.LOGIN); theme = themeProvider.getTheme(realm.getLoginTheme(), Theme.Type.LOGIN);
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to create theme", e); logger.error("Failed to create theme", e);
return Response.serverError().build(); return Response.serverError().build();

View file

@ -3,6 +3,7 @@ package org.keycloak.models.sessions.mem;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
@ -213,7 +214,11 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override @Override
public UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username) { public UsernameLoginFailureModel addUserLoginFailure(RealmModel realm, String username) {
UsernameLoginFailureKey key = new UsernameLoginFailureKey(username, realm.getId()); UsernameLoginFailureKey key = new UsernameLoginFailureKey(username, realm.getId());
return new UsernameLoginFailureAdapter(loginFailures.putIfAbsent(key, new UsernameLoginFailureEntity(username, realm.getId()))); UsernameLoginFailureEntity entity = new UsernameLoginFailureEntity(username, realm.getId());
if (loginFailures.putIfAbsent(key, entity) != null) {
throw new ModelDuplicateException();
}
return new UsernameLoginFailureAdapter(entity);
} }
@Override @Override

View file

@ -32,7 +32,7 @@ public class AerogearThemeProvider implements ThemeProvider {
} }
@Override @Override
public Theme createTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) { if (hasTheme(name, type)) {
return new ClassLoaderTheme(name, type, getClass().getClassLoader()); return new ClassLoaderTheme(name, type, getClass().getClassLoader());
} else { } else {

View file

@ -34,6 +34,7 @@
"default": "keycloak", "default": "keycloak",
"staticMaxAge": 2592000, "staticMaxAge": 2592000,
"cacheTemplates": true, "cacheTemplates": true,
"cacheThemes": true,
"folder": { "folder": {
"dir": "${jboss.server.config.dir}/themes" "dir": "${jboss.server.config.dir}/themes"
} }

View file

@ -41,6 +41,7 @@
"default": "keycloak", "default": "keycloak",
"staticMaxAge": 2592000, "staticMaxAge": 2592000,
"cacheTemplates": true, "cacheTemplates": true,
"cacheThemes": true,
"folder": { "folder": {
"dir": "${jboss.server.config.dir}/themes" "dir": "${jboss.server.config.dir}/themes"
} }

View file

@ -1,8 +1,9 @@
package org.keycloak.services.resources; package org.keycloak.services.resources;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.freemarker.ExtendingThemeManager; import org.keycloak.Config;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import javax.activation.FileTypeMap; import javax.activation.FileTypeMap;
@ -42,13 +43,13 @@ public class ThemeResource {
@Path("/{themeType}/{themeName}/{path:.*}") @Path("/{themeType}/{themeName}/{path:.*}")
public Response getResource(@PathParam("themeType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) { public Response getResource(@PathParam("themeType") String themType, @PathParam("themeName") String themeName, @PathParam("path") String path) {
try { try {
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeManager.createTheme(themeName, Theme.Type.valueOf(themType.toUpperCase())); Theme theme = themeProvider.getTheme(themeName, Theme.Type.valueOf(themType.toUpperCase()));
InputStream resource = theme.getResourceAsStream(path); InputStream resource = theme.getResourceAsStream(path);
if (resource != null) { if (resource != null) {
CacheControl cacheControl = new CacheControl(); CacheControl cacheControl = new CacheControl();
cacheControl.setNoTransform(false); cacheControl.setNoTransform(false);
cacheControl.setMaxAge(themeManager.getStaticMaxAge()); cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
return Response.ok(resource).type(mimeTypes.getContentType(path)).cacheControl(cacheControl).build(); return Response.ok(resource).type(mimeTypes.getContentType(path)).cacheControl(cacheControl).build();
} else { } else {

View file

@ -6,8 +6,9 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.freemarker.ExtendingThemeManager; import org.keycloak.Config;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -310,15 +311,15 @@ public class AdminConsole {
} }
try { try {
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeManager.createTheme(realm.getAdminTheme(), Theme.Type.ADMIN); Theme theme = themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN);
InputStream resource = theme.getResourceAsStream(path); InputStream resource = theme.getResourceAsStream(path);
if (resource != null) { if (resource != null) {
String contentType = mimeTypes.getContentType(path); String contentType = mimeTypes.getContentType(path);
CacheControl cacheControl = new CacheControl(); CacheControl cacheControl = new CacheControl();
cacheControl.setNoTransform(false); cacheControl.setNoTransform(false);
cacheControl.setMaxAge(themeManager.getStaticMaxAge()); cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1));
return Response.ok(resource).type(contentType).cacheControl(cacheControl).build(); return Response.ok(resource).type(contentType).cacheControl(cacheControl).build();
} else { } else {

View file

@ -4,6 +4,7 @@ import org.keycloak.audit.AuditListener;
import org.keycloak.authentication.AuthenticationProvider; import org.keycloak.authentication.AuthenticationProvider;
import org.keycloak.freemarker.ExtendingThemeManager; import org.keycloak.freemarker.ExtendingThemeManager;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.social.SocialProvider; import org.keycloak.social.SocialProvider;
import org.keycloak.util.ProviderLoader; import org.keycloak.util.ProviderLoader;
@ -41,11 +42,11 @@ public class ServerInfoAdminResource {
} }
private void setThemes(ServerInfoRepresentation info) { private void setThemes(ServerInfoRepresentation info) {
ExtendingThemeManager themeManager = new ExtendingThemeManager(session); ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
info.themes = new HashMap<String, List<String>>(); info.themes = new HashMap<String, List<String>>();
for (Theme.Type type : Theme.Type.values()) { for (Theme.Type type : Theme.Type.values()) {
List<String> themes = new LinkedList<String>(themeManager.nameSet(type)); List<String> themes = new LinkedList<String>(themeProvider.nameSet(type));
Collections.sort(themes); Collections.sort(themes);
info.themes.put(type.toString().toLowerCase(), themes); info.themes.put(type.toString().toLowerCase(), themes);

View file

@ -38,6 +38,7 @@
"default": "keycloak", "default": "keycloak",
"staticMaxAge": 2592000, "staticMaxAge": 2592000,
"cacheTemplates": "${keycloak.theme.cacheTemplates:true}", "cacheTemplates": "${keycloak.theme.cacheTemplates:true}",
"cacheThemes": "${keycloak.theme.cacheThemes:true}",
"folder": { "folder": {
"dir": "${keycloak.theme.dir}" "dir": "${keycloak.theme.dir}"
} }

View file

@ -30,6 +30,7 @@
"default": "keycloak", "default": "keycloak",
"staticMaxAge": 2592000, "staticMaxAge": 2592000,
"cacheTemplates": true, "cacheTemplates": true,
"cacheThemes": true,
"folder": { "folder": {
"dir": "${jboss.server.config.dir}/themes" "dir": "${jboss.server.config.dir}/themes"
} }