KEYCLOAK-6519 Theme resource provider

This commit is contained in:
stianst 2018-02-06 06:49:10 +01:00 committed by Hynek Mlnařík
parent e8de4655ac
commit 505cf5b251
30 changed files with 587 additions and 218 deletions

View file

@ -43,6 +43,7 @@
<module name="org.jboss.as.web" optional="true"/> <module name="org.jboss.as.web" optional="true"/>
<module name="org.jboss.as.version" optional="true"/> <module name="org.jboss.as.version" optional="true"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.keycloak.keycloak-wildfly-adapter" optional="true"/> <module name="org.keycloak.keycloak-wildfly-adapter" optional="true"/>
<module name="org.jboss.metadata"/> <module name="org.jboss.metadata"/>
</dependencies> </dependencies>

View file

@ -0,0 +1,56 @@
package org.keycloak.provider;
public class KeycloakDeploymentInfo {
private String name;
private boolean services;
private boolean themes;
private boolean themeResources;
public boolean isProvider() {
return services || themes || themeResources;
}
public boolean hasServices() {
return services;
}
public static KeycloakDeploymentInfo create() {
return new KeycloakDeploymentInfo();
}
private KeycloakDeploymentInfo() {
}
public KeycloakDeploymentInfo name(String name) {
this.name = name;
return this;
}
public String getName() {
return name;
}
public KeycloakDeploymentInfo services() {
this.services = true;
return this;
}
public boolean hasThemes() {
return themes;
}
public KeycloakDeploymentInfo themes() {
this.themes = true;
return this;
}
public boolean hasThemeResources() {
return themeResources;
}
public KeycloakDeploymentInfo themeResources() {
themeResources = true;
return this;
}
}

View file

@ -24,6 +24,6 @@ public interface ProviderLoaderFactory {
boolean supports(String type); boolean supports(String type);
ProviderLoader create(ClassLoader baseClassLoader, String resource); ProviderLoader create(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String resource);
} }

View file

@ -48,7 +48,6 @@ org.keycloak.email.EmailSenderSpi
org.keycloak.email.EmailTemplateSpi org.keycloak.email.EmailTemplateSpi
org.keycloak.executors.ExecutorsSpi org.keycloak.executors.ExecutorsSpi
org.keycloak.theme.ThemeSpi org.keycloak.theme.ThemeSpi
org.keycloak.theme.ThemeSelectorSpi
org.keycloak.truststore.TruststoreSpi org.keycloak.truststore.TruststoreSpi
org.keycloak.connections.httpclient.HttpClientSpi org.keycloak.connections.httpclient.HttpClientSpi
org.keycloak.models.cache.CacheRealmProviderSpi org.keycloak.models.cache.CacheRealmProviderSpi

View file

@ -40,10 +40,6 @@ public interface Theme {
URL getTemplate(String name) throws IOException; URL getTemplate(String name) throws IOException;
InputStream getTemplateAsStream(String name) throws IOException;
URL getResource(String path) throws IOException;
InputStream getResourceAsStream(String path) throws IOException; InputStream getResourceAsStream(String path) throws IOException;
/** /**

View file

@ -0,0 +1,56 @@
/*
* 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.keycloak.provider.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Set;
/**
* A theme resource provider can be used to load additional templates and resources. An example use of this would be
* a custom authenticator that requires an additional template and a JavaScript file.
*
* The theme is searched for templates and resources first. Theme resource providers are only searched if the template
* or resource is not found. This allows overriding templates and resources from theme resource providers in the theme.
*
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ThemeResourceProvider extends Provider {
/**
* Load the template for the specific name
*
* @param name the template name
* @return the URL of the template, or null if the template is unknown
* @throws IOException
*/
URL getTemplate(String name) throws IOException;
/**
* Load the resource for the specific path
*
* @param path the resource path
* @return an InputStream to read the resource, or null if the resource is unknown
* @throws IOException
*/
InputStream getResourceAsStream(String path) throws IOException;
}

View file

@ -0,0 +1,26 @@
/*
* 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.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface ThemeResourceProviderFactory extends ProviderFactory<ThemeResourceProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* 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.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ThemeResourceSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "themeResource";
}
@Override
public Class<? extends Provider> getProviderClass() {
return ThemeResourceProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return ThemeResourceProviderFactory.class;
}
}

View file

@ -33,3 +33,5 @@
# #
org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.UserStorageProviderSpi
org.keycloak.theme.ThemeResourceSpi
org.keycloak.theme.ThemeSelectorSpi

View file

@ -17,6 +17,12 @@
package org.keycloak.provider; package org.keycloak.provider;
import org.keycloak.theme.ClasspathThemeProviderFactory;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
import org.keycloak.theme.ThemeResourceSpi;
import org.keycloak.theme.ThemeSpi;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ServiceLoader; import java.util.ServiceLoader;
@ -26,27 +32,46 @@ import java.util.ServiceLoader;
*/ */
public class DefaultProviderLoader implements ProviderLoader { public class DefaultProviderLoader implements ProviderLoader {
private KeycloakDeploymentInfo info;
private ClassLoader classLoader; private ClassLoader classLoader;
public DefaultProviderLoader(ClassLoader classLoader) { public DefaultProviderLoader(KeycloakDeploymentInfo info, ClassLoader classLoader) {
this.info = info;
this.classLoader = classLoader; this.classLoader = classLoader;
} }
@Override @Override
public List<Spi> loadSpis() { public List<Spi> loadSpis() {
if (info.hasServices()) {
LinkedList<Spi> list = new LinkedList<>(); LinkedList<Spi> list = new LinkedList<>();
for (Spi spi : ServiceLoader.load(Spi.class, classLoader)) { for (Spi spi : ServiceLoader.load(Spi.class, classLoader)) {
list.add(spi); list.add(spi);
} }
return list; return list;
} else {
return Collections.emptyList();
}
} }
@Override @Override
public List<ProviderFactory> load(Spi spi) { public List<ProviderFactory> load(Spi spi) {
LinkedList<ProviderFactory> list = new LinkedList<ProviderFactory>(); List<ProviderFactory> list = new LinkedList<>();
if (info.hasServices()) {
for (ProviderFactory f : ServiceLoader.load(spi.getProviderFactoryClass(), classLoader)) { for (ProviderFactory f : ServiceLoader.load(spi.getProviderFactoryClass(), classLoader)) {
list.add(f); list.add(f);
} }
}
if (spi.getClass().equals(ThemeResourceSpi.class) && info.hasThemeResources()) {
ClasspathThemeResourceProviderFactory resourceProviderFactory = new ClasspathThemeResourceProviderFactory(info.getName(), classLoader);
list.add(resourceProviderFactory);
}
if (spi.getClass().equals(ThemeSpi.class) && info.hasThemes()) {
ClasspathThemeProviderFactory themeProviderFactory = new ClasspathThemeProviderFactory(info.getName(), classLoader);
list.add(themeProviderFactory);
}
return list; return list;
} }

View file

@ -28,8 +28,8 @@ public class DefaultProviderLoaderFactory implements ProviderLoaderFactory {
} }
@Override @Override
public ProviderLoader create(ClassLoader baseClassLoader, String resource) { public ProviderLoader create(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String resource) {
return new DefaultProviderLoader(baseClassLoader); return new DefaultProviderLoader(info, baseClassLoader);
} }
} }

View file

@ -38,8 +38,8 @@ public class FileSystemProviderLoaderFactory implements ProviderLoaderFactory {
} }
@Override @Override
public ProviderLoader create(ClassLoader baseClassLoader, String resource) { public ProviderLoader create(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String resource) {
return new DefaultProviderLoader(createClassLoader(baseClassLoader, resource.split(";"))); return new DefaultProviderLoader(info, createClassLoader(baseClassLoader, resource.split(";")));
} }
private static URLClassLoader createClassLoader(ClassLoader parent, String... files) { private static URLClassLoader createClassLoader(ClassLoader parent, String... files) {

View file

@ -21,11 +21,13 @@ import org.keycloak.common.util.MultivaluedHashMap;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -38,7 +40,7 @@ public class ProviderManager {
private MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> cache = new MultivaluedHashMap<>(); private MultivaluedHashMap<Class<? extends Provider>, ProviderFactory> cache = new MultivaluedHashMap<>();
public ProviderManager(ClassLoader baseClassLoader, String... resources) { public ProviderManager(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String... resources) {
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);
@ -46,7 +48,7 @@ public class ProviderManager {
logger.debugv("Provider loaders {0}", factories); logger.debugv("Provider loaders {0}", factories);
loaders.add(new DefaultProviderLoader(baseClassLoader)); loaders.add(new DefaultProviderLoader(info, baseClassLoader));
if (resources != null) { if (resources != null) {
for (String r : resources) { for (String r : resources) {
@ -56,7 +58,8 @@ public class ProviderManager {
boolean found = false; boolean found = false;
for (ProviderLoaderFactory f : factories) { for (ProviderLoaderFactory f : factories) {
if (f.supports(type)) { if (f.supports(type)) {
loaders.add(f.create(baseClassLoader, resource)); KeycloakDeploymentInfo resourceInfo = KeycloakDeploymentInfo.create().services();
loaders.add(f.create(resourceInfo, baseClassLoader, resource));
found = true; found = true;
break; break;
} }
@ -67,11 +70,6 @@ public class ProviderManager {
} }
} }
} }
public ProviderManager(ClassLoader baseClassLoader) {
loaders.add(new DefaultProviderLoader(baseClassLoader));
}
public synchronized List<Spi> loadSpis() { public synchronized List<Spi> loadSpis() {
// Use a map to prevent duplicates, since the loaders may have overlapping classpaths. // Use a map to prevent duplicates, since the loaders may have overlapping classpaths.
Map<String, Spi> spiMap = new HashMap<>(); Map<String, Spi> spiMap = new HashMap<>();
@ -88,15 +86,16 @@ public class ProviderManager {
public synchronized List<ProviderFactory> load(Spi spi) { public synchronized List<ProviderFactory> load(Spi spi) {
if (!cache.containsKey(spi.getProviderClass())) { if (!cache.containsKey(spi.getProviderClass())) {
IdentityHashMap factoryClasses = new IdentityHashMap();
Set<String> loaded = new HashSet<>();
for (ProviderLoader loader : loaders) { for (ProviderLoader loader : loaders) {
List<ProviderFactory> f = loader.load(spi); List<ProviderFactory> f = loader.load(spi);
if (f != null) { if (f != null) {
for (ProviderFactory pf: f) { for (ProviderFactory pf: f) {
// make sure there are no duplicates String uniqueId = spi.getName() + "-" + pf.getId();
if (!factoryClasses.containsKey(pf.getClass())) { if (!loaded.contains(uniqueId)) {
cache.add(spi.getProviderClass(), pf); cache.add(spi.getProviderClass(), pf);
factoryClasses.put(pf.getClass(), pf); loaded.add(uniqueId);
} }
} }
} }

View file

@ -22,6 +22,7 @@ import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.Provider; import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderEvent; import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener; import org.keycloak.provider.ProviderEventListener;
@ -72,7 +73,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public void init() { public void init() {
serverStartupTimestamp = System.currentTimeMillis(); serverStartupTimestamp = System.currentTimeMillis();
ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers")); ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), getClass().getClassLoader(), Config.scope().getArray("providers"));
spis.addAll(pm.loadSpis()); spis.addAll(pm.loadSpis());
factoriesMap = loadFactories(pm); factoriesMap = loadFactories(pm);
for (ProviderManager manager : ProviderManagerRegistry.SINGLETON.getPreBoot()) { for (ProviderManager manager : ProviderManagerRegistry.SINGLETON.getPreBoot()) {

View file

@ -104,16 +104,6 @@ public class ClassLoaderTheme implements Theme {
return classLoader.getResource(templateRoot + name); return classLoader.getResource(templateRoot + name);
} }
@Override
public InputStream getTemplateAsStream(String name) {
return classLoader.getResourceAsStream(templateRoot + name);
}
@Override
public URL getResource(String path) {
return classLoader.getResource(resourceRoot + path);
}
@Override @Override
public InputStream getResourceAsStream(String path) { public InputStream getResourceAsStream(String path) {
return classLoader.getResourceAsStream(resourceRoot + path); return classLoader.getResourceAsStream(resourceRoot + path);

View file

@ -25,11 +25,11 @@ import java.util.Set;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class JarThemeProvider implements ThemeProvider { public class ClasspathThemeProvider implements ThemeProvider {
private Map<Theme.Type, Map<String, ClassLoaderTheme>> themes; private Map<Theme.Type, Map<String, ClassLoaderTheme>> themes;
public JarThemeProvider(Map<Theme.Type, Map<String, ClassLoaderTheme>> themes) { public ClasspathThemeProvider(Map<Theme.Type, Map<String, ClassLoaderTheme>> themes) {
this.themes = themes; this.themes = themes;
} }

View file

@ -0,0 +1,121 @@
/*
* 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.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.util.JsonSerialization;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClasspathThemeProviderFactory implements ThemeProviderFactory {
public static final String KEYCLOAK_THEMES_JSON = "META-INF/keycloak-themes.json";
protected static Map<Theme.Type, Map<String, ClassLoaderTheme>> themes = new HashMap<>();
private String id;
public ClasspathThemeProviderFactory(String id) {
this.id = id;
}
public ClasspathThemeProviderFactory(String id, ClassLoader classLoader) {
this.id = id;
loadThemes(classLoader, classLoader.getResourceAsStream(KEYCLOAK_THEMES_JSON));
}
public static class ThemeRepresentation {
private String name;
private String[] types;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String[] getTypes() {
return types;
}
public void setTypes(String[] types) {
this.types = types;
}
}
public static class ThemesRepresentation {
private ThemeRepresentation[] themes;
public ThemeRepresentation[] getThemes() {
return themes;
}
public void setThemes(ThemeRepresentation[] themes) {
this.themes = themes;
}
}
@Override
public ThemeProvider create(KeycloakSession session) {
return new ClasspathThemeProvider(themes);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return id;
}
protected void loadThemes(ClassLoader classLoader, InputStream themesInputStream) {
try {
ThemesRepresentation themesRep = JsonSerialization.readValue(themesInputStream, ThemesRepresentation.class);
for (ThemeRepresentation themeRep : themesRep.getThemes()) {
for (String t : themeRep.getTypes()) {
Theme.Type type = Theme.Type.valueOf(t.toUpperCase());
if (!themes.containsKey(type)) {
themes.put(type, new HashMap<>());
}
themes.get(type).put(themeRep.getName(), new ClassLoaderTheme(themeRep.getName(), type, classLoader));
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to load themes", e);
}
}
}

View file

@ -0,0 +1,55 @@
package org.keycloak.theme;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class ClasspathThemeResourceProviderFactory implements ThemeResourceProviderFactory, ThemeResourceProvider {
public static final String THEME_RESOURCES_TEMPLATES = "theme-resources/templates/";
public static final String THEME_RESOURCES_RESOURCES = "theme-resources/resources/";
private final String id;
private final ClassLoader classLoader;
public ClasspathThemeResourceProviderFactory(String id, ClassLoader classLoader) {
this.id = id;
this.classLoader = classLoader;
}
@Override
public ThemeResourceProvider create(KeycloakSession session) {
return this;
}
@Override
public URL getTemplate(String name) throws IOException {
return classLoader.getResource(THEME_RESOURCES_TEMPLATES + name);
}
@Override
public InputStream getResourceAsStream(String path) throws IOException {
return classLoader.getResourceAsStream(THEME_RESOURCES_RESOURCES + path);
}
@Override
public String getId() {
return id;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}

View file

@ -111,7 +111,6 @@ public class ExtendingThemeManager implements ThemeProvider {
private Theme loadTheme(String name, Theme.Type type) throws IOException { private Theme loadTheme(String name, Theme.Type type) throws IOException {
Theme theme = findTheme(name, type); Theme theme = findTheme(name, type);
if (theme != null && (theme.getParentName() != null || theme.getImportName() != null)) {
List<Theme> themes = new LinkedList<>(); List<Theme> themes = new LinkedList<>();
themes.add(theme); themes.add(theme);
@ -132,10 +131,7 @@ public class ExtendingThemeManager implements ThemeProvider {
} }
} }
return new ExtendingTheme(themes); return new ExtendingTheme(themes, session.getAllProviders(ThemeResourceProvider.class));
} else {
return theme;
}
} }
@Override @Override
@ -178,13 +174,15 @@ public class ExtendingThemeManager implements ThemeProvider {
public static class ExtendingTheme implements Theme { public static class ExtendingTheme implements Theme {
private List<Theme> themes; private List<Theme> themes;
private Set<ThemeResourceProvider> themeResourceProviders;
private Properties properties; private Properties properties;
private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>();
public ExtendingTheme(List<Theme> themes) { public ExtendingTheme(List<Theme> themes, Set<ThemeResourceProvider> themeResourceProviders) {
this.themes = themes; this.themes = themes;
this.themeResourceProviders = themeResourceProviders;
} }
@Override @Override
@ -215,29 +213,14 @@ public class ExtendingThemeManager implements ThemeProvider {
return template; return template;
} }
} }
return null;
}
@Override for (ThemeResourceProvider t : themeResourceProviders) {
public InputStream getTemplateAsStream(String name) throws IOException { URL template = t.getTemplate(name);
for (Theme t : themes) {
InputStream template = t.getTemplateAsStream(name);
if (template != null) { if (template != null) {
return template; return template;
} }
} }
return null;
}
@Override
public URL getResource(String path) throws IOException {
for (Theme t : themes) {
URL resource = t.getResource(path);
if (resource != null) {
return resource;
}
}
return null; return null;
} }
@ -249,6 +232,14 @@ public class ExtendingThemeManager implements ThemeProvider {
return resource; return resource;
} }
} }
for (ThemeResourceProvider t : themeResourceProviders) {
InputStream resource = t.getResourceAsStream(path);
if (resource != null) {
return resource;
}
}
return null; return null;
} }

View file

@ -87,13 +87,7 @@ public class FolderTheme implements Theme {
} }
@Override @Override
public InputStream getTemplateAsStream(String name) throws IOException { public InputStream getResourceAsStream(String path) throws IOException {
URL url = getTemplate(name);
return url != null ? url.openStream() : null;
}
@Override
public URL getResource(String path) throws IOException {
if (File.separatorChar != '/') { if (File.separatorChar != '/') {
path = path.replace('/', File.separatorChar); path = path.replace('/', File.separatorChar);
} }
@ -102,16 +96,10 @@ public class FolderTheme implements Theme {
if (!file.isFile() || !file.getCanonicalPath().startsWith(resourcesDir.getCanonicalPath())) { if (!file.isFile() || !file.getCanonicalPath().startsWith(resourcesDir.getCanonicalPath())) {
return null; return null;
} else { } else {
return file.toURI().toURL(); return file.toURI().toURL().openStream();
} }
} }
@Override
public InputStream getResourceAsStream(String path) throws IOException {
URL url = getResource(path);
return url != null ? url.openStream() : null;
}
@Override @Override
public Properties getMessages(Locale locale) throws IOException { public Properties getMessages(Locale locale) throws IOException {
return getMessages("messages", locale); return getMessages("messages", locale);

View file

@ -32,47 +32,15 @@ import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class JarThemeProviderFactory implements ThemeProviderFactory { public class JarThemeProviderFactory extends ClasspathThemeProviderFactory {
protected static final String KEYCLOAK_THEMES_JSON = "META-INF/keycloak-themes.json"; public JarThemeProviderFactory() {
protected static Map<Theme.Type, Map<String, ClassLoaderTheme>> themes = new HashMap<>(); super("jar");
public static class ThemeRepresentation {
private String name;
private String[] types;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String[] getTypes() {
return types;
}
public void setTypes(String[] types) {
this.types = types;
}
}
public static class ThemesRepresentation {
private ThemeRepresentation[] themes;
public ThemeRepresentation[] getThemes() {
return themes;
}
public void setThemes(ThemeRepresentation[] themes) {
this.themes = themes;
}
} }
@Override @Override
public ThemeProvider create(KeycloakSession session) { public ThemeProvider create(KeycloakSession session) {
return new JarThemeProvider(themes); return new ClasspathThemeProvider(themes);
} }
@Override @Override
@ -88,35 +56,4 @@ public class JarThemeProviderFactory implements ThemeProviderFactory {
} }
} }
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "jar";
}
protected void loadThemes(ClassLoader classLoader, InputStream themesInputStream) {
try {
ThemesRepresentation themesRep = JsonSerialization.readValue(themesInputStream, ThemesRepresentation.class);
for (ThemeRepresentation themeRep : themesRep.getThemes()) {
for (String t : themeRep.getTypes()) {
Theme.Type type = Theme.Type.valueOf(t.toUpperCase());
if (!themes.containsKey(type)) {
themes.put(type, new HashMap<String, ClassLoaderTheme>());
}
themes.get(type).put(themeRep.getName(), new ClassLoaderTheme(themeRep.getName(), type, classLoader));
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to load themes", e);
}
}
} }

View file

@ -0,0 +1,11 @@
package org.keycloak.testsuite.theme;
import org.keycloak.theme.ClasspathThemeResourceProviderFactory;
public class TestThemeResourceProvider extends ClasspathThemeResourceProviderFactory {
public TestThemeResourceProvider() {
super("test-resources", TestThemeResourceProvider.class.getClassLoader());
}
}

View file

@ -0,0 +1,53 @@
package org.keycloak.testsuite.theme;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.theme.Theme;
import org.keycloak.theme.ThemeProvider;
import java.io.IOException;
public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
@Deployment
public static WebArchive deploy() {
return RunOnServerDeployment.create(ThemeResourceProviderTest.class, AbstractTestRealmKeycloakTest.class);
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Test
public void getTheme() {
testingClient.server().run(session -> {
try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertNotNull(theme.getTemplate("test.ftl"));
} catch (IOException e) {
Assert.fail(e.getMessage());
}
});
}
@Test
public void getResourceAsStream() {
testingClient.server().run(session -> {
try {
ThemeProvider extending = session.getProvider(ThemeProvider.class, "extending");
Theme theme = extending.getTheme("base", Theme.Type.LOGIN);
Assert.assertNotNull(theme.getResourceAsStream("test.js"));
} catch (IOException e) {
Assert.fail(e.getMessage());
}
});
}
}

View file

@ -21,6 +21,7 @@ import org.jboss.modules.Module;
import org.jboss.modules.ModuleClassLoader; import org.jboss.modules.ModuleClassLoader;
import org.jboss.modules.ModuleIdentifier; import org.jboss.modules.ModuleIdentifier;
import org.keycloak.provider.DefaultProviderLoader; import org.keycloak.provider.DefaultProviderLoader;
import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.ProviderLoader; import org.keycloak.provider.ProviderLoader;
import org.keycloak.provider.ProviderLoaderFactory; import org.keycloak.provider.ProviderLoaderFactory;
@ -35,11 +36,11 @@ public class ModuleProviderLoaderFactory implements ProviderLoaderFactory {
} }
@Override @Override
public ProviderLoader create(ClassLoader baseClassLoader, String resource) { public ProviderLoader create(KeycloakDeploymentInfo info, ClassLoader baseClassLoader, String resource) {
try { try {
Module module = Module.getContextModuleLoader().loadModule(ModuleIdentifier.fromString(resource)); Module module = Module.getContextModuleLoader().loadModule(ModuleIdentifier.fromString(resource));
ModuleClassLoader classLoader = module.getClassLoader(); ModuleClassLoader classLoader = module.getClassLoader();
return new DefaultProviderLoader(classLoader); return new DefaultProviderLoader(info, classLoader);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -21,12 +21,17 @@ import org.jboss.modules.Module;
import org.jboss.modules.ModuleClassLoader; import org.jboss.modules.ModuleClassLoader;
import org.jboss.modules.ModuleIdentifier; import org.jboss.modules.ModuleIdentifier;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.theme.ClasspathThemeProviderFactory;
import org.keycloak.theme.JarThemeProviderFactory; import org.keycloak.theme.JarThemeProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class ModuleThemeProviderFactory extends JarThemeProviderFactory { public class ModuleThemeProviderFactory extends ClasspathThemeProviderFactory {
public ModuleThemeProviderFactory() {
super("module");
}
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
@ -44,9 +49,4 @@ public class ModuleThemeProviderFactory extends JarThemeProviderFactory {
} }
} }
@Override
public String getId() {
return "module";
}
} }

View file

@ -24,12 +24,12 @@ import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.as.server.deployment.module.ModuleDependency; import org.jboss.as.server.deployment.module.ModuleDependency;
import org.jboss.as.server.deployment.module.ModuleSpecification; import org.jboss.as.server.deployment.module.ModuleSpecification;
import org.jboss.as.server.deployment.module.ResourceRoot; import org.jboss.as.server.deployment.module.ResourceRoot;
import org.jboss.logging.Logger;
import org.jboss.modules.Module; import org.jboss.modules.Module;
import org.jboss.modules.ModuleIdentifier; import org.jboss.modules.ModuleIdentifier;
import org.jboss.modules.ModuleLoader; import org.jboss.modules.ModuleLoader;
import org.jboss.vfs.VirtualFile; import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.util.AbstractVirtualFileFilterWithAttributes; import org.jboss.vfs.util.AbstractVirtualFileFilterWithAttributes;
import org.keycloak.provider.KeycloakDeploymentInfo;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -48,8 +48,6 @@ public class KeycloakProviderDependencyProcessor implements DeploymentUnitProces
private static final ModuleIdentifier RESTEASY = ModuleIdentifier.create("org.jboss.resteasy.resteasy-jaxrs"); private static final ModuleIdentifier RESTEASY = ModuleIdentifier.create("org.jboss.resteasy.resteasy-jaxrs");
private static final ModuleIdentifier APACHE = ModuleIdentifier.create("org.apache.httpcomponents"); private static final ModuleIdentifier APACHE = ModuleIdentifier.create("org.apache.httpcomponents");
private static final Logger logger = Logger.getLogger(KeycloakProviderDependencyProcessor.class);
@Override @Override
public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
@ -60,8 +58,8 @@ public class KeycloakProviderDependencyProcessor implements DeploymentUnitProces
return; return;
} }
if (!isKeycloakProviderDeployment(deploymentUnit)) return; KeycloakDeploymentInfo info = getKeycloakProviderDeploymentInfo(deploymentUnit);
if (info.hasServices()) {
final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION); final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION);
final ModuleLoader moduleLoader = Module.getBootModuleLoader(); final ModuleLoader moduleLoader = Module.getBootModuleLoader();
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_COMMON, false, false, false, false)); moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_COMMON, false, false, false, false));
@ -72,25 +70,31 @@ public class KeycloakProviderDependencyProcessor implements DeploymentUnitProces
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, RESTEASY, false, false, false, false)); moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, RESTEASY, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, APACHE, false, false, false, false)); moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, APACHE, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_JPA, false, false, false, false)); moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_JPA, false, false, false, false));
}
} }
public KeycloakProviderDependencyProcessor() { public KeycloakProviderDependencyProcessor() {
super(); super();
} }
public static boolean isKeycloakProviderDeployment(DeploymentUnit du) { public static KeycloakDeploymentInfo getKeycloakProviderDeploymentInfo(DeploymentUnit du) {
KeycloakDeploymentInfo info = KeycloakDeploymentInfo.create();
info.name(du.getName());
final ResourceRoot resourceRoot = du.getAttachment(Attachments.DEPLOYMENT_ROOT); final ResourceRoot resourceRoot = du.getAttachment(Attachments.DEPLOYMENT_ROOT);
if (resourceRoot == null) { if (resourceRoot != null) {
return false;
}
final VirtualFile deploymentRoot = resourceRoot.getRoot(); final VirtualFile deploymentRoot = resourceRoot.getRoot();
if (deploymentRoot == null || !deploymentRoot.exists()) { if (deploymentRoot != null && deploymentRoot.exists()) {
return false; if (deploymentRoot.getChild("META-INF/keycloak-themes.json").exists() && deploymentRoot.getChild("theme").exists()) {
info.themes();
} }
if (deploymentRoot.getChild("theme-resources").exists()) {
info.themeResources();
}
VirtualFile services = deploymentRoot.getChild("META-INF/services"); VirtualFile services = deploymentRoot.getChild("META-INF/services");
if (!services.exists()) return false; if(services.exists()) {
try { try {
List<VirtualFile> archives = services.getChildren(new AbstractVirtualFileFilterWithAttributes() { List<VirtualFile> archives = services.getChildren(new AbstractVirtualFileFilterWithAttributes() {
@Override @Override
@ -98,15 +102,22 @@ public class KeycloakProviderDependencyProcessor implements DeploymentUnitProces
return file.getName().startsWith("org.keycloak"); return file.getName().startsWith("org.keycloak");
} }
}); });
return !archives.isEmpty(); if (!archives.isEmpty()) {
} catch (IOException e) { info.services();
} }
return false; } catch (IOException e) {
e.printStackTrace();
}
}
}
}
return info;
} }
@Override @Override
public void undeploy(DeploymentUnit context) { public void undeploy(DeploymentUnit context) {
} }
} }

View file

@ -24,6 +24,7 @@ import org.jboss.as.server.deployment.DeploymentUnitProcessingException;
import org.jboss.as.server.deployment.DeploymentUnitProcessor; import org.jboss.as.server.deployment.DeploymentUnitProcessor;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.modules.Module; import org.jboss.modules.Module;
import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.ProviderManager; import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.ProviderManagerRegistry; import org.keycloak.provider.ProviderManagerRegistry;
@ -46,16 +47,14 @@ public class KeycloakProviderDeploymentProcessor implements DeploymentUnitProces
return; return;
} }
if (!KeycloakProviderDependencyProcessor.isKeycloakProviderDeployment(deploymentUnit)) return; KeycloakDeploymentInfo info = KeycloakProviderDependencyProcessor.getKeycloakProviderDeploymentInfo(deploymentUnit);
if (info.isProvider()) {
logger.infov("Deploying Keycloak provider: {0}", deploymentUnit.getName()); logger.infov("Deploying Keycloak provider: {0}", deploymentUnit.getName());
final Module module = deploymentUnit.getAttachment(Attachments.MODULE); final Module module = deploymentUnit.getAttachment(Attachments.MODULE);
ProviderManager pm = new ProviderManager(module.getClassLoader()); ProviderManager pm = new ProviderManager(info, module.getClassLoader());
ProviderManagerRegistry.SINGLETON.deploy(pm); ProviderManagerRegistry.SINGLETON.deploy(pm);
deploymentUnit.putAttachment(ATTACHMENT_KEY, pm); deploymentUnit.putAttachment(ATTACHMENT_KEY, pm);
}
} }
public KeycloakProviderDeploymentProcessor() { public KeycloakProviderDeploymentProcessor() {