Merge pull request #1077 from stianst/jar-theme

Jar theme
This commit is contained in:
Stian Thorgersen 2015-03-23 11:31:00 +01:00
commit 339a312d69
347 changed files with 171 additions and 92 deletions

View file

@ -2,18 +2,18 @@
<title>Themes</title> <title>Themes</title>
<para> <para>
Keycloak provides theme support for login forms and account management. This allows customizing the look Keycloak provides theme support for web pages and emails. This allows customizing the look
and feel of end-user facing pages so they can be integrated with your brand and applications. and feel of end-user facing pages so they can be integrated with your applications.
</para> </para>
<section> <section>
<title>Theme types</title> <title>Theme types</title>
<para> <para>
There are several types of themes in Keycloak: A theme can support several types to customize different aspects of Keycloak. The types currently available
are:
<itemizedlist> <itemizedlist>
<listitem>Account - Account management</listitem> <listitem>Account - Account management</listitem>
<listitem>Admin - Admin console</listitem> <listitem>Admin - Admin console</listitem>
<listitem>Common - Shared resources for themes</listitem>
<listitem>Email - Emails</listitem> <listitem>Email - Emails</listitem>
<listitem>Login - Login forms</listitem> <listitem>Login - Login forms</listitem>
<listitem>Welcome - Welcome pages</listitem> <listitem>Welcome - Welcome pages</listitem>
@ -28,6 +28,11 @@
the theme used for a realm open the <literal>Keycloak Admin Console</literal>, select your realm the theme used for a realm open the <literal>Keycloak Admin Console</literal>, select your realm
from the drop-down box in the top left corner. Under <literal>Settings</literal> click on <literal>Theme</literal>. from the drop-down box in the top left corner. Under <literal>Settings</literal> click on <literal>Theme</literal>.
</para> </para>
<para>
To set the theme for the <literal>master</literal> Keycloak admin console set the admin console theme for
the <literal>master</literal> realm. To set the theme for per realm admin access control set the admin console
theme for the corresponding realm.
</para>
<para> <para>
To change the welcome theme you need to edit <literal>standalone/configuration/keycloak-server.json</literal> To change the welcome theme you need to edit <literal>standalone/configuration/keycloak-server.json</literal>
and add <literal>welcomeTheme</literal> to the theme element, for example: and add <literal>welcomeTheme</literal> to the theme element, for example:
@ -43,9 +48,8 @@
<section> <section>
<title>Default themes</title> <title>Default themes</title>
<para> <para>
Keycloak comes bundled with default themes in <literal>standalone/configuration/themes</literal>. It is Keycloak comes bundled with default themes in <literal>standalone/configuration/themes</literal>. You should
not recommended to edit these themes directly. Instead you should create a new theme to extend a default not edit the bundled themes directly. Instead create a new theme that extends a bundled theme.
theme. A good reference is to copy the keycloak themes as these extend the base theme to add styling.
</para> </para>
</section> </section>
@ -65,24 +69,63 @@
<para> <para>
A theme can extend another theme. When extending a theme you can override individual files (templates, stylesheets, etc.). A theme can extend another theme. When extending a theme you can override individual files (templates, stylesheets, etc.).
The recommended way to create a theme is to extend the base theme. The base theme provides templates The recommended way to create a theme is to extend the base theme. The base theme provides templates
and a default message bundle. It should be possible to achieve the customization required by styling these and a default message bundle. If you decide to override templates bear in mind that you may need to update
templates. your templates when upgrading to a new release to include any changes made to the original template.
</para> </para>
<para> <para>
To create a new theme, create a folder in <literal>.../standalone/configuration/themes/&lt;theme type&gt;</literal>. Before creating a theme it's a good idea to disable caching as this makes it possible to edit theme resources
The name of the folder is the name of the theme. Then create a file <literal>theme.properties</literal> inside the theme folder. without restarting the server. To do this open <literal>../standalone/configuration/keycloak-server.json</literal>
The contents of the file should be: for <literal>theme</literal> set <literal>staticMaxAge</literal> to <literal>-1</literal> and
<literal>cacheTemplates</literal> and <literal>cacheThemes</literal> to <literal>false</literal>. For example:
<programlisting>[<![CDATA[
"theme": {
"default": "keycloak",
"staticMaxAge": 01,
"cacheTemplates": false,
"cacheThemes": false,
"folder": {
"dir": "${jboss.server.config.dir}/themes"
}
},
]]></programlisting>
Remember to re-enable caching in production as it will significantly impact performance.
</para> </para>
<programlisting>parent=base</programlisting>
<para> <para>
You have now created your theme. Check that it works by configuring it for a realm. It should look the same To create a new theme create a directory for the theme in <literal>.../standalone/configuration/themes</literal>.
as the base theme as you've not added anything to it yet. The next sections will describe how to modify The name of the directory should be the name of the theme. For example to create a theme called <literal>example-theme</literal>
the theme.</para> create the directory <literal>.../standalone/configuration/themes/example-theme</literal>. Inside the theme
directory you then need to create a directory for each of the types your theme is going to provide. For example
to add the login type to the <literal>example-theme</literal> theme create the directory
<literal>.../standalone/configuration/themes/example-theme/login</literal>.
</para>
<para>
For each type create a file <literal>theme.properties</literal> which allows setting some configuration for
the theme, for example what theme it overrides and if it should import any themes. For the above example we
want to override the base theme and import common resources from the Keycloak theme. To do this create the
file <literal>.../standalone/configuration/themes/example-theme/login/theme.properties</literal> with the
following contents:
<programlisting>[<![CDATA[
parent=base
import=common/keycloak
]]></programlisting>
</para>
<para>
You have now created a theme with support for the login type. To check that it works open the admin console.
Select your realm and click on <literal>Themes</literal>. For <literal>Login Theme</literal> select
<literal>example-theme</literal> and click <literal>Save</literal>. Then open the login page for the realm.
You can do this either by login through your application or by opening <literal>http://localhost:8080/realms/&lt;realm name&gt;/account</literal>.
</para>
<para>
To see the effect of changing the parent theme, set <literal>parent=keycloak</literal> in <literal>theme.properties</literal>
and refresh the login page. To follow the rest of the documentation set it back to <literal>parent=base</literal>
before continuing.
</para>
<section> <section>
<title>Stylesheets</title> <title>Stylesheets</title>
<para> <para>
A theme can have one or more stylesheets, to add a stylesheet create a file inside <literal>resources/css</literal> (for example <literal>resources/css/styles.css</literal>) A theme can have one or more stylesheets, to add a stylesheet create a file inside <literal>resources/css</literal>
inside your theme folder. Then registering it in <literal>theme.properties</literal> by adding: (for example <literal>resources/css/styles.css</literal>) inside your theme folder. Then registering it
in <literal>theme.properties</literal> by adding:
</para> </para>
<programlisting>styles=css/styles.css</programlisting> <programlisting>styles=css/styles.css</programlisting>
<para> <para>
@ -90,6 +133,17 @@
as you want. For example: as you want. For example:
</para> </para>
<programlisting>styles=css/styles.css css/more-styles.css</programlisting> <programlisting>styles=css/styles.css css/more-styles.css</programlisting>
For the example-theme above add <literal>example-theme/login/resources/css/styles.css</literal> with the
following content:
<programlisting>[<![CDATA[
#kc-form {
background-color: #000;
color: #fff;
padding: 20px;
}]]></programlisting>
Then edit <literal>example-theme/login/theme.properties</literal> and add <programlisting>styles=css/styles.css</programlisting>.
Refresh the login page to see your changes. It's not pretty, but you can see how easily you can modify the
styles for your theme.
</section> </section>
<section> <section>
<title>Scripts</title> <title>Scripts</title>
@ -121,9 +175,8 @@
<section> <section>
<title>Messages</title> <title>Messages</title>
<para> <para>
Text in the templates are loaded from message bundles. Currently internationalization isn't supported, Text in the templates are loaded from message bundles. A theme that extends another theme will inherit
but that will be added in a later release. A theme that extends another theme will inherit all messages all messages from the parents message bundle, but can override individual messages. For example to replace
from the parents message bundle, but can override individual messages. For example to replace
<literal>Username</literal> on the login form with <literal>Your Username</literal> create the file <literal>Username</literal> on the login form with <literal>Your Username</literal> create the file
<literal>messages/messages.properties</literal> inside your theme folder and add the following content: <literal>messages/messages.properties</literal> inside your theme folder and add the following content:
</para> </para>
@ -134,30 +187,79 @@
<para> <para>
Keycloak uses <ulink url="http://freemarker.org">Freemarker Templates</ulink> in order to generate HTML. Keycloak uses <ulink url="http://freemarker.org">Freemarker Templates</ulink> in order to generate HTML.
These templates are defined in <literal>.ftl</literal> files and can be overriden from the base theme. These templates are defined in <literal>.ftl</literal> files and can be overriden from the base theme.
Check out the Freemarker website on how to form a template file. Check out the Freemarker website on how to form a template file. To override the login template for the
<literal>example-theme</literal> copy <literal>../standalone/configuration/themes/base/login/login.ftl</literal>
to <literal>../standalone/configuration/themes/example-theme/login</literal> and open it in an editor. After
the first line (&lt;#import ...&gt;) add <literal>&lt;h1&gt;HELLO WORLD!&lt;/h1&gt;</literal> then refresh
the page.
</para> </para>
</section> </section>
</section> </section>
<section>
<title>Deploying themes</title>
<para>
Themes can be deployed to Keycloak by copying the theme directory to <literal>../standalone/configuration/themes</literal>
or it can be deployed as a module. For a single server or during development just copying the theme is fine, but
in a cluster or domain it's recommended to deploy as a module.
</para>
<para>
To deploy a theme as a module you need to create an jar (it's basically just a zip with jar extension) with
the theme resources and a file <literal>META/keycloak-server.json</literal> that describes the themes contained
in the archive. For example <literal>example-theme.jar</literal> with the contents:
<itemizedlist>
<listitem>META-INF/keycloak-themes.json</listitem>
<listitem>theme/example-theme/login/theme.properties</listitem>
<listitem>theme/example-theme/login/login.ftl</listitem>
<listitem>theme/example-theme/login/resources/css/styles.css</listitem>
</itemizedlist>
The contents of META-INF/keycloak-server.json in this case would be:
<programlisting>[<![CDATA[
{
"themes": [{
"name" : "example-theme",
"types": [ "login" ]
}]
}
]]></programlisting>
As you can see a single jar can contain multiple themes and each theme can support one or more types.
</para>
<para>
The deploy the jar as a module to Keycloak you can either manually create the module or use <literal>jboss-cli</literal>.
It's simplest to use <literal>jboss-cli</literal> as it creates the required directories and module descriptor
for you. To deploy the above jar <literal>jboss-cli</literal> run:
<programlisting>[<![CDATA[
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.example.exampletheme --resources=example-theme.jar"
]]></programlisting>
If you're on windows run <programlisting>KEYCLOAK_HOME/bin/jboss-cli.bat</programlisting>.
</para>
<para>
This command creates <literal>modules/org/example/exampletheme/main</literal> containing <literal>example-theme.jar</literal>
and <literal>module.xml</literal>.
</para>
<para>
Once you've created the module you need to register it with Keycloak do this by editing
<literal>../standalone/configuration/keycloak-server.json</literal> and adding the module to <literal>theme/module/modules</literal>. For example:
<programlisting>[<![CDATA[
"theme": {
...
"module": {
"modules": [ "org.example.exampletheme" ]
}
}
]]></programlisting>
</para>
<para>
If a theme is deployed to <literal>../standalone/configuration/themes</literal> and as a module the first
is used.
</para>
</section>
<section> <section>
<title>SPIs</title> <title>SPIs</title>
<para> <para>
For full control of login forms and account management Keycloak provides a number of SPIs. For full control of login forms and account management Keycloak provides a number of SPIs.
</para> </para>
<section>
<title>Theme SPI</title>
<para>
The Theme SPI allows creating different mechanisms to load themes for the default FreeMarker based
implementations of login forms and account management. To create a theme provider you will need to implement
<literal>org.keycloak.freemarker.ThemeProviderFactory</literal> and <literal>org.keycloak.freemarker.ThemeProvider</literal>.
</para>
<para>
Keycloak comes with two theme providers, one that loads themes from the classpath (used by default themes)
and another that loads themes from a folder (used by custom themes). Looking at these
would be a good place to start to create your own theme provider. You can find them inside
<literal>forms/common-themes</literal> on GitHub or the source download.
</para>
</section>
<section> <section>
<title>Account SPI</title> <title>Account SPI</title>
<para> <para>

View file

@ -21,7 +21,7 @@ Then open $KEYCLOAK_HOME/standalone/configuration/keycloak-server.json and regis
} }
} }
Alternatively you can copy `src/main/resources/theme/login` to `standalone/configuration/themes/login/`. Alternatively you can copy `src/main/resources/theme/sunrise` to `standalone/configuration/themes/`.
Once you've added the theme open the admin console, select your realm, click on `Theme`. In the dropdown for `Login Theme` select `sunrise`. Click `Save` and login to the realm to see the new theme in action. Once you've added the theme open the admin console, select your realm, click on `Theme`. In the dropdown for `Login Theme` select `sunrise`. Click `Save` and login to the realm to see the new theme in action.
@ -46,13 +46,7 @@ Then open $KEYCLOAK_HOME/standalone/configuration/keycloak-server.json and regis
} }
} }
Alternatively you can copy: Alternatively you can copy `src/main/resources/theme/logo-example` to `standalone/configuration/themes/`.
* `account/logo-example` to `standalone/configuration/themes/account/`
* `login/logo-example` to `standalone/configuration/themes/login/`
* `admin/logo-example` to `standalone/configuration/themes/admin/`
* `welcome/logo-example` to `standalone/configuration/themes/welcome/`
Once you've added the theme open the admin console, select your realm, click on `Theme`. In the dropdowns for `Login Theme`, `Account Theme` and `Admin Console Theme` select `logo-example`. Click `Save` and login to the realm to see the new theme in action. Once you've added the theme open the admin console, select your realm, click on `Theme`. In the dropdowns for `Login Theme`, `Account Theme` and `Admin Console Theme` select `logo-example`. Click `Save` and login to the realm to see the new theme in action.

View file

@ -11,9 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public class ExtendingThemeManagerFactory implements ThemeProviderFactory { public class ExtendingThemeManagerFactory implements ThemeProviderFactory {
private ConcurrentHashMap<ThemeKey, Theme> themeCache = new ConcurrentHashMap<ThemeKey, Theme>(); private ConcurrentHashMap<ThemeKey, Theme> themeCache;
private ExtendingThemeManager themeManager;
@Override @Override
public ThemeProvider create(KeycloakSession session) { public ThemeProvider create(KeycloakSession session) {
@ -23,7 +21,7 @@ public class ExtendingThemeManagerFactory implements ThemeProviderFactory {
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
if(Config.scope("theme").getBoolean("cacheThemes", true)) { if(Config.scope("theme").getBoolean("cacheThemes", true)) {
themeCache = new ConcurrentHashMap<ThemeKey, Theme>(); themeCache = new ConcurrentHashMap<>();
} }
} }

View file

@ -20,7 +20,7 @@ public class FreeMarkerUtil {
public FreeMarkerUtil() { public FreeMarkerUtil() {
if (Config.scope("theme").getBoolean("cacheTemplates", true)) { if (Config.scope("theme").getBoolean("cacheTemplates", true)) {
cache = new ConcurrentHashMap<String, Template>(); cache = new ConcurrentHashMap<>();
} }
} }

View file

@ -2,13 +2,11 @@ package org.keycloak.theme;
import org.keycloak.freemarker.Theme; import org.keycloak.freemarker.Theme;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Locale; import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import java.util.ResourceBundle;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -42,7 +40,7 @@ public class ClassLoaderTheme implements Theme {
this.type = type; this.type = type;
this.classLoader = classLoader; this.classLoader = classLoader;
String themeRoot = "theme/" + type.toString().toLowerCase() + "/" + name + "/"; String themeRoot = "theme/" + name + "/" + type.toString().toLowerCase() + "/";
this.templateRoot = themeRoot; this.templateRoot = themeRoot;
this.resourceRoot = themeRoot + "resources/"; this.resourceRoot = themeRoot + "resources/";
@ -60,10 +58,6 @@ public class ClassLoaderTheme implements Theme {
} }
} }
public ClassLoaderTheme() {
}
@Override @Override
public String getName() { public String getName() {
return name; return name;

View file

@ -18,11 +18,13 @@ public class FolderTheme implements Theme {
private String parentName; private String parentName;
private String importName; private String importName;
private File themeDir; private File themeDir;
private String name;
private Type type; private Type type;
private final Properties properties; private final Properties properties;
public FolderTheme(File themeDir, Type type) throws IOException { public FolderTheme(File themeDir, String name, Type type) throws IOException {
this.themeDir = themeDir; this.themeDir = themeDir;
this.name = name;
this.type = type; this.type = type;
this.properties = new Properties(); this.properties = new Properties();
@ -36,7 +38,7 @@ public class FolderTheme implements Theme {
@Override @Override
public String getName() { public String getName() {
return themeDir.getName(); return name;
} }
@Override @Override

View file

@ -15,10 +15,10 @@ import java.util.Set;
*/ */
public class FolderThemeProvider implements ThemeProvider { public class FolderThemeProvider implements ThemeProvider {
private File rootDir; private File themesDir;
public FolderThemeProvider(File rootDir) { public FolderThemeProvider(File themesDir) {
this.rootDir = rootDir; this.themesDir = themesDir;
} }
@Override @Override
@ -28,51 +28,41 @@ public class FolderThemeProvider implements ThemeProvider {
@Override @Override
public Theme getTheme(String name, Theme.Type type) throws IOException { public Theme getTheme(String name, Theme.Type type) throws IOException {
if (hasTheme(name, type)) { File themeDir = getThemeDir(name, type);
return new FolderTheme(new File(getTypeDir(type), name), type); return themeDir.isDirectory() ? new FolderTheme(themeDir, name, type) : null;
}
return null;
} }
@Override @Override
public Set<String> nameSet(Theme.Type type) { public Set<String> nameSet(Theme.Type type) {
File typeDir = getTypeDir(type); final String typeName = type.name().toLowerCase();
if (typeDir != null) { File[] themeDirs = themesDir.listFiles(new FileFilter() {
File[] themes = typeDir.listFiles(new FileFilter() {
@Override @Override
public boolean accept(File pathname) { public boolean accept(File pathname) {
return pathname.isDirectory(); return pathname.isDirectory() && new File(pathname, typeName).isDirectory();
} }
}); });
if (themeDirs != null) {
Set<String> names = new HashSet<String>(); Set<String> names = new HashSet<String>();
for (File t : themes) { for (File themeDir : themeDirs) {
names.add(t.getName()); names.add(themeDir.getName());
} }
return names; return names;
} } else {
return Collections.emptySet(); return Collections.emptySet();
} }
private File getTypeDir(Theme.Type type) {
if (rootDir != null && rootDir.isDirectory()) {
File typeDir = new File(rootDir, type.name().toLowerCase());
if (typeDir.isDirectory()) {
return typeDir;
}
}
return null;
} }
@Override @Override
public boolean hasTheme(String name, Theme.Type type) { public boolean hasTheme(String name, Theme.Type type) {
File typeDir = getTypeDir(type); return getThemeDir(name, type).isDirectory();
return typeDir != null && new File(typeDir, name).isDirectory();
} }
@Override @Override
public void close() { public void close() {
} }
private File getThemeDir(String name, Theme.Type type) {
return new File(themesDir, name + File.separator + type.name().toLowerCase());
}
} }

Some files were not shown because too many files have changed in this diff Show more