KEYCLOAK-8044 Clear theme caches on hot-deploy
This commit is contained in:
parent
d8d81ee162
commit
9e47022116
13 changed files with 347 additions and 380 deletions
|
@ -34,4 +34,6 @@ public interface ThemeManager {
|
|||
*/
|
||||
Set<String> nameSet(Theme.Type type);
|
||||
|
||||
void clearCache();
|
||||
|
||||
}
|
||||
|
|
|
@ -36,11 +36,13 @@ public class ProviderManager {
|
|||
|
||||
private static final Logger logger = Logger.getLogger(ProviderManager.class);
|
||||
|
||||
private final KeycloakDeploymentInfo info;
|
||||
private List<ProviderLoader> loaders = new LinkedList<ProviderLoader>();
|
||||
private MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> cache = new MultivaluedHashMap<>();
|
||||
|
||||
|
||||
public ProviderManager(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String... resources) {
|
||||
this.info = info;
|
||||
List<ProviderLoaderFactory> factories = new LinkedList<ProviderLoaderFactory>();
|
||||
for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class, getClass().getClassLoader())) {
|
||||
factories.add(f);
|
||||
|
@ -126,4 +128,8 @@ public class ProviderManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
public synchronized KeycloakDeploymentInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ import org.keycloak.sessions.AuthenticationSessionProvider;
|
|||
import org.keycloak.storage.ClientStorageManager;
|
||||
import org.keycloak.storage.UserStorageManager;
|
||||
import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
||||
import org.keycloak.theme.DefaultThemeManager;
|
||||
import org.keycloak.vault.DefaultVaultTranscriber;
|
||||
import org.keycloak.vault.VaultProvider;
|
||||
import org.keycloak.vault.VaultTranscriber;
|
||||
|
@ -301,7 +300,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
|
|||
@Override
|
||||
public ThemeManager theme() {
|
||||
if (themeManager == null) {
|
||||
themeManager = new DefaultThemeManager(this);
|
||||
themeManager = factory.getThemeManagerFactory().create(this);
|
||||
}
|
||||
return themeManager;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.provider.ProviderManagerDeployer;
|
|||
import org.keycloak.provider.ProviderManagerRegistry;
|
||||
import org.keycloak.provider.Spi;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||
import org.keycloak.theme.DefaultThemeManagerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
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<>();
|
||||
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
|
||||
protected long serverStartupTimestamp;
|
||||
|
||||
|
@ -101,7 +104,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
|||
|
||||
AdminPermissions.registerListener(this);
|
||||
|
||||
themeManagerFactory = new DefaultThemeManagerFactory();
|
||||
}
|
||||
|
||||
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() {
|
||||
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>();
|
||||
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) {
|
||||
factory.postInit(this);
|
||||
}
|
||||
|
||||
if (pm.getInfo().hasThemes() || pm.getInfo().hasThemeResources()) {
|
||||
themeManagerFactory.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -166,6 +175,10 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
|||
}
|
||||
}
|
||||
|
||||
protected DefaultThemeManagerFactory getThemeManagerFactory() {
|
||||
return themeManagerFactory;
|
||||
}
|
||||
|
||||
protected void checkProvider() {
|
||||
for (Spi spi : spis) {
|
||||
String provider = Config.getProvider(spi.getName());
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.ThemeProvider;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.keycloak.services.managers.AppAuthManager;
|
|||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.ThemeProvider;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
import javax.ws.rs.InternalServerErrorException;
|
||||
|
|
|
@ -53,9 +53,7 @@ import org.keycloak.representations.info.ServerInfoRepresentation;
|
|||
import org.keycloak.representations.info.SpiInfoRepresentation;
|
||||
import org.keycloak.representations.info.SystemInfoRepresentation;
|
||||
import org.keycloak.representations.info.ThemeInfoRepresentation;
|
||||
import org.keycloak.storage.user.ImportSynchronization;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.ThemeProvider;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Produces;
|
||||
|
|
283
services/src/main/java/org/keycloak/theme/DefaultThemeManager.java
Normal file → Executable file
283
services/src/main/java/org/keycloak/theme/DefaultThemeManager.java
Normal file → Executable 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;
|
||||
|
||||
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.ThemeManager;
|
||||
|
||||
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.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 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.defaultTheme = Config.scope("theme").get("default", Version.NAME.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Theme getTheme(Theme.Type type) throws IOException {
|
||||
public Theme getTheme(Theme.Type type) {
|
||||
String name = session.getProvider(ThemeSelectorProvider.class).getThemeName(type);
|
||||
return getTheme(name, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Theme getTheme(String name, Theme.Type type) throws IOException {
|
||||
return session.getProvider(ThemeProvider.class, "extending").getTheme(name, type);
|
||||
public Theme getTheme(String name, Theme.Type 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
|
||||
public Set<String> nameSet(Theme.Type type) {
|
||||
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
|
||||
return themeProvider.nameSet(type);
|
||||
Set<String> themes = new HashSet<String>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,43 +17,63 @@
|
|||
|
||||
package org.keycloak.theme;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ThemeManager;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
@Override
|
||||
public ThemeProvider create(KeycloakSession session) {
|
||||
return new ExtendingThemeManager(session, themeCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
public DefaultThemeManagerFactory() {
|
||||
if(Config.scope("theme").getBoolean("cacheThemes", true)) {
|
||||
themeCache = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
public ThemeManager create(KeycloakSession session) {
|
||||
return new DefaultThemeManager(this, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public Theme getCachedTheme(String name, Theme.Type type) {
|
||||
if (themeCache != null) {
|
||||
DefaultThemeManagerFactory.ThemeKey key = DefaultThemeManagerFactory.ThemeKey.get(name, type);
|
||||
return themeCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "extending";
|
||||
public Theme addCachedTheme(String name, Theme.Type type, Theme theme) {
|
||||
if (theme == null) {
|
||||
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 {
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,6 +15,5 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.theme.ExtendingThemeManagerFactory
|
||||
org.keycloak.theme.JarThemeProviderFactory
|
||||
org.keycloak.theme.FolderThemeProviderFactory
|
|
@ -8,7 +8,6 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
|
|||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.ThemeProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
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>
|
||||
*/
|
||||
@AuthServerContainerExclude(REMOTE)
|
||||
public class ExtendingThemeTest extends AbstractKeycloakTest {
|
||||
public class DefaultThemeManagerTest extends AbstractKeycloakTest {
|
||||
|
||||
private static final String THEME_NAME = "environment-agnostic";
|
||||
|
||||
@Before
|
||||
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
|
||||
|
@ -39,8 +41,7 @@ public class ExtendingThemeTest extends AbstractKeycloakTest {
|
|||
ContainerAssume.assumeAuthServerUndertow();
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme(THEME_NAME, Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().getTheme(THEME_NAME, Theme.Type.LOGIN);
|
||||
Assert.assertEquals("Keycloak is awesome", theme.getProperties().getProperty("system.property.found"));
|
||||
Assert.assertEquals("${missing_system_property}", theme.getProperties().getProperty("system.property.missing"));
|
||||
Assert.assertEquals("defaultValue", theme.getProperties().getProperty("system.property.missing.with.default"));
|
||||
|
@ -55,8 +56,7 @@ public class ExtendingThemeTest extends AbstractKeycloakTest {
|
|||
public void environmentVariablesSubstitutionInThemeProperties() {
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme(THEME_NAME, Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().getTheme(THEME_NAME, Theme.Type.LOGIN);
|
||||
Assert.assertEquals("${env.MISSING_ENVIRONMENT_VARIABLE}", theme.getProperties().getProperty("env.missing"));
|
||||
Assert.assertEquals("defaultValue", theme.getProperties().getProperty("env.missingWithDefault"));
|
||||
if (System.getenv().containsKey("HOMEPATH")) {
|
|
@ -5,14 +5,12 @@ import org.junit.Test;
|
|||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.theme.ThemeProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
||||
@AuthServerContainerExclude(AuthServer.REMOTE)
|
||||
public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
|
@ -25,8 +23,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
public void getTheme() {
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
|
||||
Assert.assertNotNull(theme.getTemplate("test.ftl"));
|
||||
} catch (IOException e) {
|
||||
Assert.fail(e.getMessage());
|
||||
|
@ -38,8 +35,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
public void getResourceAsStream() {
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
|
||||
Assert.assertNotNull(theme.getResourceAsStream("test.js"));
|
||||
} catch (IOException e) {
|
||||
Assert.fail(e.getMessage());
|
||||
|
@ -51,8 +47,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
public void getMessages() {
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().getTheme("base", Theme.Type.LOGIN);
|
||||
Assert.assertNotNull(theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-8818"));
|
||||
Assert.assertNotEquals("Full name (Theme-resources)", theme.getMessages("messages", Locale.ENGLISH).get("fullName"));
|
||||
} catch (IOException e) {
|
||||
|
@ -68,8 +63,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
public void getMessagesLocaleResolving() {
|
||||
testingClient.server().run(session -> {
|
||||
try {
|
||||
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
|
||||
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
|
||||
Theme theme = session.theme().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", theme.getMessages("messages", new Locale("en", "US")).get("test.keycloak-12926"));
|
||||
Assert.assertEquals("Test en", theme.getMessages("messages", Locale.ENGLISH).get("test.keycloak-12926"));
|
||||
|
|
Loading…
Reference in a new issue