templates.docs.miscellaneous.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{#==========================================
Docs : "Miscellaneous"
==========================================#}
Miscellaneous
{#==========================================
Dictionary
==========================================#}
Dictionary (i18n / internationalization)
Usage
The Dictionary interface
represents the object in which you store and from which you get localized messages for a multilingual application.
You get a localized message by injecting the Dictionary in a class
and by specifying the key
of the message to get :
public class MyClass {
private final Dictionary dictionary;
@Inject
public MyClass(Dictionary dictionary) {
this.dictionary = dictionary;
}
protected Dictionary getDictionary() {
return this.dictionary;
}
public void someMethod() {
String localizedMessage = getDictionary().get("some.message.key");
System.out.println(localizedMessage);
}
}
In this example, the message key is "some.message.key
"
By default, the Locale
used to pick the right version of the message is the one returned
by the Locale Resolver. But you can also specify the Locale as a parameter:
String localizedMessage = getDictionary().get("some.message.key", Locale.JAPANESE);
System.out.println(localizedMessage);
The default Dictionary implementation,
SpincastDictionaryDefault,
uses the Templating Engine to evaluate messages. This means that you can pass parameters
when getting a message. For example :
{% verbatim %}
// Let's say the "some.message.key" message in the dictionary is :
// "Hi {{userName}}! My name is {{authorName}}."
String localizedMessage = getDictionary().get("some.message.key",
Pair.of("userName", user.name),
Pair.of("authorName", admin.name));
{% endverbatim %}
Note that, to improve performance, a message from the dictionary is only evaluated using the
Templating Engine if at least one parameter is passed when getting it! Otherwise, the
message is going to be returned as is, without any evaluation. If you have
a message that doesn't require any parameter but still needs to be evaluated, you can force the
evaluation using the "forceEvaluation
" parameter :
String localizedMessage = getDictionary().get("some.message.key", true);
Finally, note that Spincast also provides a msg(...)
function to get a localized message from a template (a HTML
template, for example).
Adding messages to the dictionary
By default, only some Spincast core messages and some plugins messages will be added to the Dictionary.
To add your own messages, the ones required in your application, you extend the
SpincastDictionaryDefault
base class and you override the addMessages()
method. By using the key()
and
msg()
helpers, you then specify the localized messages of your application.
For example :
{% verbatim %}
public class AppDictionary extends SpincastDictionaryDefault {
@Inject
public AppDictionary(LocaleResolver localeResolver,
TemplatingEngine templatingEngine,
AppConfigs appConfig,
Set<DictionaryEntries> dictionaryEntries) {
super(localeResolver, templatingEngine, appConfig, dictionaryEntries);
}
@Override
protected void addMessages() {
key("users.home.welcome",
msg("", "Hi {{name}}!"),
msg("fr", "Salut {{name}}!"));
key("users.profile.title",
msg("", "Your profile"),
msg("fr", "Votre profil"),
msg("ja", "あなたのプロフィール"));
// ...
}
}
{% endverbatim %}
The first parameter of the msg()
helper
is the language of the message. The empty language (""
) is called the fallback language or
default language. It is english in our example. If a message is requested using a specific Locale but is not found,
Spincast will return the message using the fallback language, if it exists.
The default Dictionary implementation,
SpincastDictionaryDefault,
uses the Templating Engine to evaluate messages. This means that you can perform logic inside
your messages ("if
", "for loops
"...
anything supported by Pebble) and you can use parameters, as we
can see with the {% verbatim %}"{{name}}
"{% endverbatim %} part in the previous example.
Finally, don't forget to register your custom implementation of the
Dictionary interface (in our example:
"AppDictionary
") in your Guice context! For example :
{% verbatim %}
public class AppModule extends SpincastGuiceModuleBase {
@Override
protected void configure() {
bind(Dictionary.class).to(AppDictionary.class).in(Scopes.SINGLETON);
//...
}
}
{% endverbatim %}
Overriding core messages and plugins messages
Spincast core messages and plugins's messages are added to the Dictionary before you add
your own messages using the addMessage()
method. This means that you can override them
and even translate them in a new language, if required.
The plugins should provide public constants representing the keys of the messages they use. This way,
you can easily know how to override them. For example, the keys of Spincast's core messages are provided as constants
in the
SpincastCoreDictionaryEntriesDefault class.
Each plugins should similarly list the keys of their messages in their respective documentation.
Here's an example of overriding a core Spincast message:
{% verbatim %}
public class AppDictionary extends SpincastDictionaryDefault {
@Inject
public AppDictionary(LocaleResolver localeResolver,
TemplatingEngine templatingEngine,
AppConfigs appConfig,
Set<DictionaryEntries> dictionaryEntries) {
super(localeResolver, templatingEngine, appConfig, dictionaryEntries);
}
@Override
protected void addMessages() {
// Override a core message!
key(SpincastCoreDictionaryEntriesDefault.MESSAGE_KEY_ROUTE_NOT_FOUND_DEFAULTMESSAGE,
msg("", "my custom message!"));
// Then, add your custom messages...
}
}
{% endverbatim %}
Configuration
A configuration for the dictionary is available through SpincastConfig:
-
DictionaryEntryNotFoundBehavior getDictionaryEntryNotFoundFallbackTo()
When a requested message key is not found in the dictionary, this configuration specifies what
happens.
-
EXCEPTION :
a DictionaryKeyNotFoundException
exception is thrown.
-
RETURN_EMPTY_STRING :
an empty string is returned.
-
RETURN_KEY :
the requested message key itself is returned.
By default, when development mode is enabled, an exception is thrown. Otherwise, an empty string
is returned.
Note that if a version of the message exists with the fallback language (""
),
it will always be found.
{#==========================================
Flash Messages
==========================================#}
Flash messages
A Flash message
is a message that is displayed to the user only once.
It is most of the time used to display
a confirmation message to the user when a form is submitted and the page
redirected.
A good practice on a website is indeed to redirect the user to a new
page when a form has been POSTed and is valid : that way, even if the
user refreshes the resulting page, the form
won't be resubmitted. But this pattern leads to a question : how to display
a confirmation message on the page the user is redirected to? Flash
messages are the answer to this question.
(All the Forms & Validation demos
use Flash messages
to display a confirmation message when the Form is
submitted and is valid... Try them!)
A Flash message
is most of the time used to display a confirmation
(success) message to the user, but it, in fact, supports three "levels" :
-
Success
-
Warning
-
Error
A Flash message can also have some variables associated with it
(in the form of an JsonObject
),
and those variables can be used when the Flash message
is retrieved.
You can specify a Flash message
:
-
By using the
.response().redirect(...)
method
in a Route Handler
public void myHandler(AppRequestContext context) {
context.response().redirect("/some-url/",
FlashMessageLevel.SUCCESS,
"The form has been processed successfully.");
}
-
By throwing a RedirectException
public void myHandler(AppRequestContext context) {
throw new RedirectException("/some-url/",
FlashMessageLevel.SUCCESS,
"The form has been processed successfully.");
}
Of course, it doesn't make sense to specify a Flash message
when redirecting the user to an external website!
Flash messages
, when retrieved, are automatically added as a
global variable for the Templating Engine
. It is, in fact, converted to
an Alert message.
How are Flash messages
actually implemented? If Spincast
has validated that the user supports cookies, it uses one
to store the "id" of the Flash message
and to retrieve it
on the page the user is redirected to. If cookies are disabled,
or if their support has not been validated yet, the "id" of the
Flash message
is added as a queryString parameter to
the URL of the page the user is redirected to.
{#==========================================
Alert Messages
==========================================#}
Alert messages
{% if alertDemoMsg is not empty | default(false) %}
✔ {{alertDemoMsg}}
{% endif %}
An Alert message
is a message that has a Success
,
Warning
or Error
level and that is displayed to
the user, usually at the top of the page or of a section. It aims to inform
the user about the result of an action, for example.
There are multiple ways to display an Alert message on a website.
The Spincast website uses Toastr.js
when javascript is enabled , and a plain
<div>
when javascript is disabled
( Try it!)
An Alert message is simply some text and a level associated with it,
both added as a templating variable. You can easily implement your own
way to pass such messages to be displayed to the user, but Spincast suggests a
convention and some utilities :
-
The Alert messages are provided to the
Templating Engine
as a variable associated with the "spincast.alerts"
key.
-
There is a dedicated
"addAlert(...)"
method on the
response()
add-on to easily add one from a Route handler
:
public void myHandler(AppRequestContext context) {
context.response().addAlert(AlertLevel.ERROR,
"Your submission is not valid!");
}
Note that Flash messages will be automatically
converted to Alert messages when it's time to render a template!
This means that as long as you add code to display Alert messages in your interface,
Flash messages will also be displayed properly.
{#==========================================
SSL
==========================================#}
Using a SSL certificate (HTTPS)
It is recommended that you serve your application over HTTPS
and
not HTTP
, which is not secure. To achieve that, you need to install a
SSL certificate
.
If you download the Quick Start application, you will
find two files explaining the required procedure :
-
/varia/ssl_certificate_howto/self-signed.txt
Shows how to use a self-signed
certificate, for development
purpose.
-
/varia/ssl_certificate_howto/lets-encrypt.txt
Shows how to use a Let's Encrypt
certificate. Let's Encrypt
is a provider of free, but totally valid,
SSL certificates. Instructions in this file will probably work for certificates
obtained from other providers, but we haven't tested it yet.
{#==========================================
Spincast Utilities
==========================================#}
Spincast Utilities
Spincast provides some generic utilities, accessible via the
SpincastUtils
interface :
-
void zipDirectory(File directoryToZip, File targetZipFile, boolean includeDirItself)
Zips a directory.
-
void zipExtract(File zipFile, File targetDir)
Extracts a .zip file to the specified directory.
-
String getMimeTypeFromMultipleSources(String responseContentTypeHeader, String resourcePath, String requestPath)
Gets the mime type
using multiple sources of information.
-
String getMimeTypeFromPath(String path)
Gets the mime type
from a path, using its extension.
-
String getMimeTypeFromExtension(String extension)
Gets the mime type
from the extension.
-
Locale getLocaleBestMatchFromAcceptLanguageHeader(String acceptLanguageHeader)
Gets the best Locale to use given a "Accept-Language" HTTP header.
-
boolean isContentTypeToSkipGziping(String contentType)
Should the specified Content-Type
be gzipped?
-
String getSpincastCurrentVersion()
Gets the current Spincast version.
-
String getCacheBusterCode()
The cache buster to use.
This should probably change each time
the application is restarted or at least redeployed.
It should also be in such a format that it's possible to
remove it from a given text.
This must be kept in sync with
removeCacheBusterCode
!
-
String removeCacheBusterCodes(String text)
Removes the cache buster code occurences from the
given text.
Note that this won't simply remove the current
cache busting code, it will remove any valid cache busting code...
This is what we want since we don't want a client sending a request
containing an old cache busting code to break!
This must be kept in sync with
getCacheBusterCode
!
-
String readClasspathFile(String path)
Reads a file on the classpath and returns it as a
String.
Paths are always considered from the root at the classpath.
You can start the path with a "/" or not, it makes no difference.
Uses UTF-8
by default.
-
InputStream getClasspathInputStream(String path)
Reads a file on the classpath and returns it as an InputStream.
Paths are always considered from the root at the classpath.
You can start the path with a "/" or not, it makes no difference.
Important : the calling code is the one
responsible to close the inputstream!
-
void copyClasspathFileToFileSystem(String classpathFilePath, File targetFile)
Copy a file from the classpath to the system file.
If the target file already exists, it is overwritten.
-
void copyClasspathDirToFileSystem(String classpathDirPath, File targetDir)
Recursively copy a directory from the classpath to the file system.
If the target directory already exists, it is overwritten.
-
boolean isClasspathResourceLoadedFromJar(String resourcePath)
Return true
if the classpath resource
is located inside a .jar file. If it is a regular file located on the file system,
false
is returned.
Throws an exception if the resource is not found.
-
boolean isClassLoadedFromJar(Class<?> clazz)
Return true
if the specified Class
was loaded from a
.jar file and not from a standalone .class
file on the file system.
-
File getClassLocationDirOrJarFile(Class<?> clazz)
The location of the specified class.
This will be a directory if the class was loaded from the file system as
a standalone .class
file or a .jar file if the class was loaded
from a jar.
You can use isClassLoadedFromJar() to
determine if the class was loaded from a .jar file or not.
-
boolean isRunningFromExecutableJar()
Return true
if the application is currently running from an
executable Fat Jar. Returns false
otherwise, for example when the
application is started from an IDE.
-
File getAppJarDirectory()
Returns the directory where the executable .jar from which the application is running
is located.
@Returns null
if the application is currently not
running from an executable .jar file (for example if it was started from an IDE).
-
File getAppRootDirectoryNoJar()
Returns the root directory where the application is running, when the application
is not running from an executable .jar file.
@Returns null
if the application is running from
an executable .jar file.
-
boolean isPortOpen(String host, int port)
Validate if a port is open on the specified host.
-
String convertToFriendlyToken(String str)
Converts a string so it can be used in an URL without
being escaped: remove accents, spaces, etc.
This can be used to create "friendly token" in an SEO
optimized URL. It can also be used to create a human friendly
file name from a random string.
Be careful if you plan on using the result of this
method as a unique token since many strings may
result in the same thing!
Returns the string with only a-z
, "-
" and "_
" characters.
Or, if the resulting string is empty, a random UUID
is returned.
{#==========================================
@MainArgs
==========================================#}
@MainArgs
The init(...)
method of the Bootstrapper allows you
to bind the arguments received in your main(...)
method. For example :
public static void main(String[] args) {
Spincast.configure()
.module(new AppModule())
.init(args);
//....
}
By doing so, those arguments will be available for injection, using the @MainArgs
annotation :
public class AppConfig extends SpincastConfig implements AppConfig {
private final String[] mainArgs;
@Inject
public AppConfig(@MainArgs String[] mainArgs) {
this.mainArgs = mainArgs;
}
protected String[] getMainArgs() {
return this.mainArgs;
}
@Override
public int getHttpServerPort() {
int port = super.getHttpServerPort();
if(getMainArgs().length > 0) {
port = Integer.parseInt(getMainArgs()[0]);
}
return port;
}
}
{#==========================================
Using an init() method
==========================================#}
Using an init()
method
This is more about standard Guice development than about Spincast, but we
feel it's a useful thing to know.
Guice doesn't provide
support for a @PostConstruct
annotation out of the box.
And since it is often seen as a bad practice to do too much work directly in a constructor, what we want
is an init()
method to be called once the
object it fully constructed, and do the initialization work there.
The trick is that Guice calls any @Inject
annotated methods
once the object is created, so let's use this to our advantage :
public class UserService implements UserService {
private final SpincastConfig spincastConfig;
@Inject
public UserService(SpincastConfig spincastConfig) {
this.spincastConfig = spincastConfig;
}
@Inject
protected void init() {
doSomeValidation();
doSomeInitialization();
}
//...
}
Explanation :
-
5-8 : The constructor's job is only to
receive the dependencies.
-
10-14 : An
init()
method is
also annotated with @Inject
. This method will be called once the
object is fully constructed. This is a good place to do some initialization work!
What we recommend is constructor injection + one (and only one) @Inject
annotated method. The problem with multiple @Inject
annotated methods (other than
constructors) is that it's hard to know in which order they will be called.
Finally, if the init()
method must be called as soon as the application starts, make sure
you bind the object using
asEagerSingleton()!
{#==========================================
==========================================#}
Server started listeners
Spincast provides a hook so you can be informed when your application's HTTP server
has been successfully started. For a class to be called:
1. The class must implement the
ServerStartedListener
interface. When the server is started, the serverStartedSuccessfully()
method will be called:
public class MyClass implements ServerStartedListener {
@Override
public void serverStartedSuccessfully() {
System.out.println("Server started hook!");
}
//...
}
Note that each listener is called in a new Thread.
2. The class must be registered on the Multibinder<ServerStartedListener>
multibinder, in your application's Guice module:
Multibinder<ServerStartedListener> serverStartedListenersMultibinder =
Multibinder.newSetBinder(binder(), ServerStartedListener.class);
serverStartedListenersMultibinder.addBinding().to(MyClass.class).in(Scopes.SINGLETON);