From 9e4702211651c66187e0394fbaaa8eef7cca5f74 Mon Sep 17 00:00:00 2001 From: stianst Date: Wed, 19 Feb 2020 10:17:52 +0100 Subject: [PATCH] KEYCLOAK-8044 Clear theme caches on hot-deploy --- .../org/keycloak/models/ThemeManager.java | 2 + .../keycloak/provider/ProviderManager.java | 6 + .../services/DefaultKeycloakSession.java | 3 +- .../DefaultKeycloakSessionFactory.java | 13 + .../services/resources/ThemeResource.java | 1 - .../resources/account/AccountLoader.java | 1 - .../admin/info/ServerInfoAdminResource.java | 2 - .../keycloak/theme/DefaultThemeManager.java | 283 ++++++++++++++- ...y.java => DefaultThemeManagerFactory.java} | 54 ++- .../keycloak/theme/ExtendingThemeManager.java | 331 ------------------ .../org.keycloak.theme.ThemeProviderFactory | 1 - ...Test.java => DefaultThemeManagerTest.java} | 14 +- .../theme/ThemeResourceProviderTest.java | 16 +- 13 files changed, 347 insertions(+), 380 deletions(-) mode change 100644 => 100755 services/src/main/java/org/keycloak/theme/DefaultThemeManager.java rename services/src/main/java/org/keycloak/theme/{ExtendingThemeManagerFactory.java => DefaultThemeManagerFactory.java} (65%) delete mode 100755 services/src/main/java/org/keycloak/theme/ExtendingThemeManager.java rename testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/{ExtendingThemeTest.java => DefaultThemeManagerTest.java} (82%) diff --git a/server-spi/src/main/java/org/keycloak/models/ThemeManager.java b/server-spi/src/main/java/org/keycloak/models/ThemeManager.java index 399b6283f7..c7f6022a17 100644 --- a/server-spi/src/main/java/org/keycloak/models/ThemeManager.java +++ b/server-spi/src/main/java/org/keycloak/models/ThemeManager.java @@ -34,4 +34,6 @@ public interface ThemeManager { */ Set nameSet(Theme.Type type); + void clearCache(); + } diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java index 9355a7c343..2c9d0617d5 100644 --- a/services/src/main/java/org/keycloak/provider/ProviderManager.java +++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java @@ -36,11 +36,13 @@ public class ProviderManager { private static final Logger logger = Logger.getLogger(ProviderManager.class); + private final KeycloakDeploymentInfo info; private List loaders = new LinkedList(); private MultivaluedHashMap, ProviderFactory> cache = new MultivaluedHashMap<>(); public ProviderManager(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String... resources) { + this.info = info; List factories = new LinkedList(); 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; + } + } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 61714056b5..51fa5c10d2 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -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; } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index b4f0f46c6b..56f3a778f5 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -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, Map> factoriesMap = new HashMap<>(); protected CopyOnWriteArrayList 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, Map> getFactoriesCopy() { Map, Map> copy = new HashMap<>(); for (Map.Entry, Map> 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()); diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java index 67b96cf0cc..a959a04c44 100644 --- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java @@ -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; diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java index 8efd0900bc..79329cf5e3 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountLoader.java @@ -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; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 808358d99a..7361da8772 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -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; diff --git a/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java b/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java old mode 100644 new mode 100755 index c539eea608..13b20f48e7 --- a/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java +++ b/services/src/main/java/org/keycloak/theme/DefaultThemeManager.java @@ -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 Stian Thorgersen + */ 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 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 nameSet(Theme.Type type) { - ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); - return themeProvider.nameSet(type); + Set themes = new HashSet(); + 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 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 themes; + private Set themeResourceProviders; + + private Properties properties; + + private ConcurrentHashMap> messages = new ConcurrentHashMap<>(); + + public ExtendingTheme(List themes, Set 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 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()); + 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 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 getProviders() { + if (providers == null) { + providers = new LinkedList(session.getAllProviders(ThemeProvider.class)); + Collections.sort(providers, (o1, o2) -> o2.getProviderPriority() - o1.getProviderPriority()); + } + + return providers; + } + } diff --git a/services/src/main/java/org/keycloak/theme/ExtendingThemeManagerFactory.java b/services/src/main/java/org/keycloak/theme/DefaultThemeManagerFactory.java similarity index 65% rename from services/src/main/java/org/keycloak/theme/ExtendingThemeManagerFactory.java rename to services/src/main/java/org/keycloak/theme/DefaultThemeManagerFactory.java index 4ce280cd72..8edb3829df 100644 --- a/services/src/main/java/org/keycloak/theme/ExtendingThemeManagerFactory.java +++ b/services/src/main/java/org/keycloak/theme/DefaultThemeManagerFactory.java @@ -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 Stian Thorgersen */ -public class ExtendingThemeManagerFactory implements ThemeProviderFactory { +public class DefaultThemeManagerFactory { + + private static final Logger log = Logger.getLogger(DefaultThemeManagerFactory.class); private ConcurrentHashMap 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 { diff --git a/services/src/main/java/org/keycloak/theme/ExtendingThemeManager.java b/services/src/main/java/org/keycloak/theme/ExtendingThemeManager.java deleted file mode 100755 index dda329a3b1..0000000000 --- a/services/src/main/java/org/keycloak/theme/ExtendingThemeManager.java +++ /dev/null @@ -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 Stian Thorgersen - */ -public class ExtendingThemeManager implements ThemeProvider { - - private static final Logger log = Logger.getLogger(ExtendingThemeManager.class); - - private final KeycloakSession session; - private final ConcurrentHashMap themeCache; - private List providers; - private String defaultTheme; - - public ExtendingThemeManager(KeycloakSession session, ConcurrentHashMap themeCache) { - this.session = session; - this.themeCache = themeCache; - this.defaultTheme = Config.scope("theme").get("default", Version.NAME.toLowerCase()); - } - - private List 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() { - @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 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 nameSet(Theme.Type type) { - Set themes = new HashSet(); - 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 themes; - private Set themeResourceProviders; - - private Properties properties; - - private ConcurrentHashMap> messages = new ConcurrentHashMap<>(); - - public ExtendingTheme(List themes, Set 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 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()); - 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 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; - } - -} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.theme.ThemeProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.theme.ThemeProviderFactory index 1d8690bd9b..91ec01f8c0 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.theme.ThemeProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.theme.ThemeProviderFactory @@ -15,6 +15,5 @@ # limitations under the License. # -org.keycloak.theme.ExtendingThemeManagerFactory org.keycloak.theme.JarThemeProviderFactory org.keycloak.theme.FolderThemeProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ExtendingThemeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/DefaultThemeManagerTest.java similarity index 82% rename from testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ExtendingThemeTest.java rename to testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/DefaultThemeManagerTest.java index c7020d4bee..4e657a8515 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ExtendingThemeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/DefaultThemeManagerTest.java @@ -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 Vincent Letarouilly */ @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")) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java index 9add6a169c..99126ab9f2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java @@ -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"));