KEYCLOAK-8044 Clear theme caches on hot-deploy

This commit is contained in:
stianst 2020-02-19 10:17:52 +01:00 committed by Stian Thorgersen
parent d8d81ee162
commit 9e47022116
13 changed files with 347 additions and 380 deletions

View file

@ -34,4 +34,6 @@ public interface ThemeManager {
*/ */
Set<String> nameSet(Theme.Type type); Set<String> nameSet(Theme.Type type);
void clearCache();
} }

View file

@ -36,11 +36,13 @@ public class ProviderManager {
private static final Logger logger = Logger.getLogger(ProviderManager.class); private static final Logger logger = Logger.getLogger(ProviderManager.class);
private final KeycloakDeploymentInfo info;
private List<ProviderLoader> loaders = new LinkedList<ProviderLoader>(); private List<ProviderLoader> loaders = new LinkedList<ProviderLoader>();
private MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> cache = new MultivaluedHashMap<>(); private MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> cache = new MultivaluedHashMap<>();
public ProviderManager(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String... resources) { public ProviderManager(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String... resources) {
this.info = info;
List<ProviderLoaderFactory> factories = new LinkedList<ProviderLoaderFactory>(); List<ProviderLoaderFactory> factories = new LinkedList<ProviderLoaderFactory>();
for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class, getClass().getClassLoader())) { for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class, getClass().getClassLoader())) {
factories.add(f); factories.add(f);
@ -126,4 +128,8 @@ public class ProviderManager {
return null; return null;
} }
public synchronized KeycloakDeploymentInfo getInfo() {
return info;
}
} }

View file

@ -41,7 +41,6 @@ import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.storage.ClientStorageManager; import org.keycloak.storage.ClientStorageManager;
import org.keycloak.storage.UserStorageManager; import org.keycloak.storage.UserStorageManager;
import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.theme.DefaultThemeManager;
import org.keycloak.vault.DefaultVaultTranscriber; import org.keycloak.vault.DefaultVaultTranscriber;
import org.keycloak.vault.VaultProvider; import org.keycloak.vault.VaultProvider;
import org.keycloak.vault.VaultTranscriber; import org.keycloak.vault.VaultTranscriber;
@ -301,7 +300,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
@Override @Override
public ThemeManager theme() { public ThemeManager theme() {
if (themeManager == null) { if (themeManager == null) {
themeManager = new DefaultThemeManager(this); themeManager = factory.getThemeManagerFactory().create(this);
} }
return themeManager; return themeManager;
} }

View file

@ -32,6 +32,7 @@ import org.keycloak.provider.ProviderManagerDeployer;
import org.keycloak.provider.ProviderManagerRegistry; import org.keycloak.provider.ProviderManagerRegistry;
import org.keycloak.provider.Spi; import org.keycloak.provider.Spi;
import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.theme.DefaultThemeManagerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -50,6 +51,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>(); private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>();
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<>(); protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<>();
private DefaultThemeManagerFactory themeManagerFactory;
// TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps // TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps
protected long serverStartupTimestamp; protected long serverStartupTimestamp;
@ -101,7 +104,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
AdminPermissions.registerListener(this); AdminPermissions.registerListener(this);
themeManagerFactory = new DefaultThemeManagerFactory();
} }
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() { protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>(); Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>();
for (Map.Entry<Class<? extends Provider>, Map<String, ProviderFactory>> entry : factoriesMap.entrySet()) { for (Map.Entry<Class<? extends Provider>, Map<String, ProviderFactory>> entry : factoriesMap.entrySet()) {
@ -141,6 +146,10 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
for (ProviderFactory factory : deployed) { for (ProviderFactory factory : deployed) {
factory.postInit(this); factory.postInit(this);
} }
if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) {
themeManagerFactory.clearCache();
}
} }
@Override @Override
@ -166,6 +175,10 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
} }
} }
protected DefaultThemeManagerFactory getThemeManagerFactory() {
return themeManagerFactory;
}
protected void checkProvider() { protected void checkProvider() {
for (Spi spi : spis) { for (Spi spi : spis) {
String provider = Config.getProvider(spi.getName()); String provider = Config.getProvider(spi.getName());

View file

@ -23,7 +23,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;

View file

@ -28,7 +28,6 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.InternalServerErrorException;

View file

@ -53,9 +53,7 @@ import org.keycloak.representations.info.ServerInfoRepresentation;
import org.keycloak.representations.info.SpiInfoRepresentation; import org.keycloak.representations.info.SpiInfoRepresentation;
import org.keycloak.representations.info.SystemInfoRepresentation; import org.keycloak.representations.info.SystemInfoRepresentation;
import org.keycloak.representations.info.ThemeInfoRepresentation; import org.keycloak.representations.info.ThemeInfoRepresentation;
import org.keycloak.storage.user.ImportSynchronization;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;

View file

@ -1,34 +1,303 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.theme; package org.keycloak.theme;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Version;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ThemeManager; import org.keycloak.models.ThemeManager;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
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>
*/
public class DefaultThemeManager implements ThemeManager { public class DefaultThemeManager implements ThemeManager {
private KeycloakSession session; private static final Logger log = Logger.getLogger(DefaultThemeManager.class);
public DefaultThemeManager(KeycloakSession session) { private final DefaultThemeManagerFactory factory;
private final KeycloakSession session;
private List<ThemeProvider> providers;
private String defaultTheme;
public DefaultThemeManager(DefaultThemeManagerFactory factory, KeycloakSession session) {
this.factory = factory;
this.session = session; this.session = session;
this.defaultTheme = Config.scope("theme").get("default", Version.NAME.toLowerCase());
} }
@Override @Override
public Theme getTheme(Theme.Type type) throws IOException { public Theme getTheme(Theme.Type type) {
String name = session.getProvider(ThemeSelectorProvider.class).getThemeName(type); String name = session.getProvider(ThemeSelectorProvider.class).getThemeName(type);
return getTheme(name, type); return getTheme(name, type);
} }
@Override @Override
public Theme getTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) {
return session.getProvider(ThemeProvider.class, "extending").getTheme(name, type); if (name == null) {
name = defaultTheme;
}
Theme theme = factory.getCachedTheme(name, type);
if (theme == null) {
theme = loadTheme(name, type);
if (theme == null) {
theme = loadTheme("keycloak", type);
if (theme == null) {
theme = loadTheme("base", type);
}
log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
} else {
theme = factory.addCachedTheme(name, type, theme);
}
}
return theme;
} }
@Override @Override
public Set<String> nameSet(Theme.Type type) { public Set<String> nameSet(Theme.Type type) {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Set<String> themes = new HashSet<String>();
return themeProvider.nameSet(type); for (ThemeProvider p : getProviders()) {
themes.addAll(p.nameSet(type));
}
return themes;
} }
@Override
public void clearCache() {
factory.clearCache();
}
private Theme loadTheme(String name, Theme.Type type) {
Theme theme = findTheme(name, type);
List<Theme> themes = new LinkedList<>();
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
if (theme.getParentName() != null) {
for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
theme = findTheme(parentName, type);
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
}
}
return new ExtendingTheme(themes, session.getAllProviders(ThemeResourceProvider.class));
}
private Theme findTheme(String name, Theme.Type type) {
for (ThemeProvider p : getProviders()) {
if (p.hasTheme(name, type)) {
try {
return p.getTheme(name, type);
} catch (IOException e) {
log.errorv(e, p.getClass() + " failed to load theme, type={0}, name={1}", type, name);
}
}
}
return null;
}
private static class ExtendingTheme implements Theme {
private List<Theme> themes;
private Set<ThemeResourceProvider> themeResourceProviders;
private Properties properties;
private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>();
public ExtendingTheme(List<Theme> themes, Set<ThemeResourceProvider> themeResourceProviders) {
this.themes = themes;
this.themeResourceProviders = themeResourceProviders;
}
@Override
public String getName() {
return themes.get(0).getName();
}
@Override
public String getParentName() {
return themes.get(0).getParentName();
}
@Override
public String getImportName() {
return themes.get(0).getImportName();
}
@Override
public Type getType() {
return themes.get(0).getType();
}
@Override
public URL getTemplate(String name) throws IOException {
for (Theme t : themes) {
URL template = t.getTemplate(name);
if (template != null) {
return template;
}
}
for (ThemeResourceProvider t : themeResourceProviders) {
URL template = t.getTemplate(name);
if (template != null) {
return template;
}
}
return null;
}
@Override
public InputStream getResourceAsStream(String path) throws IOException {
for (Theme t : themes) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
for (ThemeResourceProvider t : themeResourceProviders) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
return null;
}
@Override
public Properties getMessages(Locale locale) throws IOException {
return getMessages("messages", locale);
}
@Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
if (messages.get(baseBundlename) == null || messages.get(baseBundlename).get(locale) == null) {
Properties messages = new Properties();
Locale parent = getParent(locale);
if (parent != null) {
messages.putAll(getMessages(baseBundlename, parent));
}
for (ThemeResourceProvider t : themeResourceProviders ){
messages.putAll(t.getMessages(baseBundlename, locale));
}
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties m = itr.previous().getMessages(baseBundlename, locale);
if (m != null) {
messages.putAll(m);
}
}
this.messages.putIfAbsent(baseBundlename, new ConcurrentHashMap<Locale, Properties>());
this.messages.get(baseBundlename).putIfAbsent(locale, messages);
return messages;
} else {
return messages.get(baseBundlename).get(locale);
}
}
@Override
public Properties getProperties() throws IOException {
if (properties == null) {
Properties properties = new Properties();
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties p = itr.previous().getProperties();
if (p != null) {
properties.putAll(p);
}
}
substituteProperties(properties);
this.properties = properties;
return properties;
} else {
return properties;
}
}
/**
* Iterate over all string properties defined in "theme.properties" then substitute the value with system property or environment variables.
* See {@link StringPropertyReplacer#replaceProperties} for details about the different formats.
*/
private void substituteProperties(final Properties properties) {
for (final String propertyName : properties.stringPropertyNames()) {
properties.setProperty(propertyName, StringPropertyReplacer.replaceProperties(properties.getProperty(propertyName), new SystemEnvProperties()));
}
}
}
private static Locale getParent(Locale locale) {
if (Locale.ENGLISH.equals(locale)) {
return null;
}
if (locale.getVariant() != null && !locale.getVariant().isEmpty()) {
return new Locale(locale.getLanguage(), locale.getCountry());
}
if (locale.getCountry() != null && !locale.getCountry().isEmpty()) {
return new Locale(locale.getLanguage());
}
return Locale.ENGLISH;
}
private List<ThemeProvider> getProviders() {
if (providers == null) {
providers = new LinkedList(session.getAllProviders(ThemeProvider.class));
Collections.sort(providers, (o1, o2) -> o2.getProviderPriority() - o1.getProviderPriority());
}
return providers;
}
} }

View file

@ -17,43 +17,63 @@
package org.keycloak.theme; package org.keycloak.theme;
import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ThemeManager;
import java.util.concurrent.ConcurrentHashMap; 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 ExtendingThemeManagerFactory implements ThemeProviderFactory { public class DefaultThemeManagerFactory {
private static final Logger log = Logger.getLogger(DefaultThemeManagerFactory.class);
private ConcurrentHashMap<ThemeKey, Theme> themeCache; private ConcurrentHashMap<ThemeKey, Theme> themeCache;
@Override public DefaultThemeManagerFactory() {
public ThemeProvider create(KeycloakSession session) {
return new ExtendingThemeManager(session, themeCache);
}
@Override
public void init(Config.Scope config) {
if(Config.scope("theme").getBoolean("cacheThemes", true)) { if(Config.scope("theme").getBoolean("cacheThemes", true)) {
themeCache = new ConcurrentHashMap<>(); themeCache = new ConcurrentHashMap<>();
} }
} }
@Override public ThemeManager create(KeycloakSession session) {
public void postInit(KeycloakSessionFactory factory) { return new DefaultThemeManager(this, session);
} }
@Override public Theme getCachedTheme(String name, Theme.Type type) {
public void close() { if (themeCache != null) {
DefaultThemeManagerFactory.ThemeKey key = DefaultThemeManagerFactory.ThemeKey.get(name, type);
return themeCache.get(key);
} else {
return null;
}
} }
@Override public Theme addCachedTheme(String name, Theme.Type type, Theme theme) {
public String getId() { if (theme == null) {
return "extending"; return null;
}
if (themeCache == null) {
return theme;
}
DefaultThemeManagerFactory.ThemeKey key = DefaultThemeManagerFactory.ThemeKey.get(name, type);
if (themeCache.putIfAbsent(key, theme) != null) {
theme = themeCache.get(key);
}
return theme;
}
public void clearCache() {
if (themeCache != null) {
themeCache.clear();
log.info("Cleared theme cache");
}
} }
public static class ThemeKey { public static class ThemeKey {

View file

@ -1,331 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.theme;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Version;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.models.KeycloakSession;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ExtendingThemeManager implements ThemeProvider {
private static final Logger log = Logger.getLogger(ExtendingThemeManager.class);
private final KeycloakSession session;
private final ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache;
private List<ThemeProvider> providers;
private String defaultTheme;
public ExtendingThemeManager(KeycloakSession session, ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache) {
this.session = session;
this.themeCache = themeCache;
this.defaultTheme = Config.scope("theme").get("default", Version.NAME.toLowerCase());
}
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;
}
@Override
public int getProviderPriority() {
return 0;
}
@Override
public Theme getTheme(String name, Theme.Type type) throws IOException {
if (name == null) {
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 (theme == null) {
theme = loadTheme("keycloak", type);
if (theme == null) {
theme = loadTheme("base", type);
}
log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
} else 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);
List<Theme> themes = new LinkedList<>();
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
if (theme.getParentName() != null) {
for (String parentName = theme.getParentName(); parentName != null; parentName = theme.getParentName()) {
theme = findTheme(parentName, type);
themes.add(theme);
if (theme.getImportName() != null) {
String[] s = theme.getImportName().split("/");
themes.add(findTheme(s[1], Theme.Type.valueOf(s[0].toUpperCase())));
}
}
}
return new ExtendingTheme(themes, session.getAllProviders(ThemeResourceProvider.class));
}
@Override
public Set<String> nameSet(Theme.Type type) {
Set<String> themes = new HashSet<String>();
for (ThemeProvider p : getProviders()) {
themes.addAll(p.nameSet(type));
}
return themes;
}
@Override
public boolean hasTheme(String name, Theme.Type type) {
for (ThemeProvider p : getProviders()) {
if (p.hasTheme(name, type)) {
return true;
}
}
return false;
}
@Override
public void close() {
providers = null;
}
private Theme findTheme(String name, Theme.Type type) {
for (ThemeProvider p : getProviders()) {
if (p.hasTheme(name, type)) {
try {
return p.getTheme(name, type);
} catch (IOException e) {
log.errorv(e, p.getClass() + " failed to load theme, type={0}, name={1}", type, name);
}
}
}
return null;
}
public static class ExtendingTheme implements Theme {
private List<Theme> themes;
private Set<ThemeResourceProvider> themeResourceProviders;
private Properties properties;
private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>();
public ExtendingTheme(List<Theme> themes, Set<ThemeResourceProvider> themeResourceProviders) {
this.themes = themes;
this.themeResourceProviders = themeResourceProviders;
}
@Override
public String getName() {
return themes.get(0).getName();
}
@Override
public String getParentName() {
return themes.get(0).getParentName();
}
@Override
public String getImportName() {
return themes.get(0).getImportName();
}
@Override
public Type getType() {
return themes.get(0).getType();
}
@Override
public URL getTemplate(String name) throws IOException {
for (Theme t : themes) {
URL template = t.getTemplate(name);
if (template != null) {
return template;
}
}
for (ThemeResourceProvider t : themeResourceProviders) {
URL template = t.getTemplate(name);
if (template != null) {
return template;
}
}
return null;
}
@Override
public InputStream getResourceAsStream(String path) throws IOException {
for (Theme t : themes) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
for (ThemeResourceProvider t : themeResourceProviders) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
return null;
}
@Override
public Properties getMessages(Locale locale) throws IOException {
return getMessages("messages", locale);
}
@Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
if (messages.get(baseBundlename) == null || messages.get(baseBundlename).get(locale) == null) {
Properties messages = new Properties();
Locale parent = getParent(locale);
if (parent != null) {
messages.putAll(getMessages(baseBundlename, parent));
}
for (ThemeResourceProvider t : themeResourceProviders ){
messages.putAll(t.getMessages(baseBundlename, locale));
}
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties m = itr.previous().getMessages(baseBundlename, locale);
if (m != null) {
messages.putAll(m);
}
}
this.messages.putIfAbsent(baseBundlename, new ConcurrentHashMap<Locale, Properties>());
this.messages.get(baseBundlename).putIfAbsent(locale, messages);
return messages;
} else {
return messages.get(baseBundlename).get(locale);
}
}
@Override
public Properties getProperties() throws IOException {
if (properties == null) {
Properties properties = new Properties();
ListIterator<Theme> itr = themes.listIterator(themes.size());
while (itr.hasPrevious()) {
Properties p = itr.previous().getProperties();
if (p != null) {
properties.putAll(p);
}
}
substituteProperties(properties);
this.properties = properties;
return properties;
} else {
return properties;
}
}
/**
* Iterate over all string properties defined in "theme.properties" then substitute the value with system property or environment variables.
* See {@link StringPropertyReplacer#replaceProperties} for details about the different formats.
*/
private void substituteProperties(final Properties properties) {
for (final String propertyName : properties.stringPropertyNames()) {
properties.setProperty(propertyName, StringPropertyReplacer.replaceProperties(properties.getProperty(propertyName), new SystemEnvProperties()));
}
}
}
private static Locale getParent(Locale locale) {
if (Locale.ENGLISH.equals(locale)) {
return null;
}
if (locale.getVariant() != null && !locale.getVariant().isEmpty()) {
return new Locale(locale.getLanguage(), locale.getCountry());
}
if (locale.getCountry() != null && !locale.getCountry().isEmpty()) {
return new Locale(locale.getLanguage());
}
return Locale.ENGLISH;
}
}

View file

@ -15,6 +15,5 @@
# limitations under the License. # limitations under the License.
# #
org.keycloak.theme.ExtendingThemeManagerFactory
org.keycloak.theme.JarThemeProviderFactory org.keycloak.theme.JarThemeProviderFactory
org.keycloak.theme.FolderThemeProviderFactory org.keycloak.theme.FolderThemeProviderFactory

View file

@ -8,7 +8,6 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.util.ContainerAssume; import org.keycloak.testsuite.util.ContainerAssume;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -19,13 +18,16 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
* @author <a href="mailto:vincent.letarouilly@gmail.com">Vincent Letarouilly</a> * @author <a href="mailto:vincent.letarouilly@gmail.com">Vincent Letarouilly</a>
*/ */
@AuthServerContainerExclude(REMOTE) @AuthServerContainerExclude(REMOTE)
public class ExtendingThemeTest extends AbstractKeycloakTest { public class DefaultThemeManagerTest extends AbstractKeycloakTest {
private static final String THEME_NAME = "environment-agnostic"; private static final String THEME_NAME = "environment-agnostic";
@Before @Before
public void setUp() { public void setUp() {
testingClient.server().run(session -> System.setProperty("existing_system_property", "Keycloak is awesome")); testingClient.server().run(session -> {
System.setProperty("existing_system_property", "Keycloak is awesome");
session.theme().clearCache();
});
} }
@Override @Override
@ -39,8 +41,7 @@ public class ExtendingThemeTest extends AbstractKeycloakTest {
ContainerAssume.assumeAuthServerUndertow(); ContainerAssume.assumeAuthServerUndertow();
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme(THEME_NAME, Theme.Type.LOGIN);
Theme theme = extending.getTheme(THEME_NAME, Theme.Type.LOGIN);
Assert.assertEquals("Keycloak is awesome", theme.getProperties().getProperty("system.property.found")); Assert.assertEquals("Keycloak is awesome", theme.getProperties().getProperty("system.property.found"));
Assert.assertEquals("${missing_system_property}", theme.getProperties().getProperty("system.property.missing")); Assert.assertEquals("${missing_system_property}", theme.getProperties().getProperty("system.property.missing"));
Assert.assertEquals("defaultValue", theme.getProperties().getProperty("system.property.missing.with.default")); Assert.assertEquals("defaultValue", theme.getProperties().getProperty("system.property.missing.with.default"));
@ -55,8 +56,7 @@ public class ExtendingThemeTest extends AbstractKeycloakTest {
public void environmentVariablesSubstitutionInThemeProperties() { public void environmentVariablesSubstitutionInThemeProperties() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme(THEME_NAME, Theme.Type.LOGIN);
Theme theme = extending.getTheme(THEME_NAME, Theme.Type.LOGIN);
Assert.assertEquals("${env.MISSING_ENVIRONMENT_VARIABLE}", theme.getProperties().getProperty("env.missing")); Assert.assertEquals("${env.MISSING_ENVIRONMENT_VARIABLE}", theme.getProperties().getProperty("env.missing"));
Assert.assertEquals("defaultValue", theme.getProperties().getProperty("env.missingWithDefault")); Assert.assertEquals("defaultValue", theme.getProperties().getProperty("env.missingWithDefault"));
if (System.getenv().containsKey("HOMEPATH")) { if (System.getenv().containsKey("HOMEPATH")) {

View file

@ -5,14 +5,12 @@ import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.theme.Theme; import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@AuthServerContainerExclude(AuthServer.REMOTE) @AuthServerContainerExclude(AuthServer.REMOTE)
public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest { public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
@ -25,8 +23,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
public void getTheme() { public void getTheme() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertNotNull(theme.getTemplate("test.ftl")); Assert.assertNotNull(theme.getTemplate("test.ftl"));
} catch (IOException e) { } catch (IOException e) {
Assert.fail(e.getMessage()); Assert.fail(e.getMessage());
@ -38,8 +35,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
public void getResourceAsStream() { public void getResourceAsStream() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertNotNull(theme.getResourceAsStream("test.js")); Assert.assertNotNull(theme.getResourceAsStream("test.js"));
} catch (IOException e) { } catch (IOException e) {
Assert.fail(e.getMessage()); Assert.fail(e.getMessage());
@ -51,8 +47,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
public void getMessages() { public void getMessages() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertNotNull(theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-8818")); Assert.assertNotNull(theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-8818"));
Assert.assertNotEquals("Full name (Theme-resources)", theme.getMessages("messages", Locale.ENGLISH).get("fullName")); Assert.assertNotEquals("Full name (Theme-resources)", theme.getMessages("messages", Locale.ENGLISH).get("fullName"));
} catch (IOException e) { } catch (IOException e) {
@ -68,8 +63,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
public void getMessagesLocaleResolving() { public void getMessagesLocaleResolving() {
testingClient.server().run(session -> { testingClient.server().run(session -> {
try { try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending"); Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertEquals("Test en_US_variant", theme.getMessages("messages", new Locale("en", "US", "variant")).get("test.keycloak-12926")); Assert.assertEquals("Test en_US_variant", theme.getMessages("messages", new Locale("en", "US", "variant")).get("test.keycloak-12926"));
Assert.assertEquals("Test en_US", theme.getMessages("messages", new Locale("en", "US")).get("test.keycloak-12926")); Assert.assertEquals("Test en_US", theme.getMessages("messages", new Locale("en", "US")).get("test.keycloak-12926"));
Assert.assertEquals("Test en", theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-12926")); Assert.assertEquals("Test en", theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-12926"));