+ * Set it before page method which should be executed on specified
+ * action. Action is a string parameter with name "action".
+ *
+ *
+ * If you have set @Action without value, it means that default action
+ * became the annotated method but not action().
+ *
+ *
+ * If there is validation method (see @Validate.class) action method
+ * will be invoked only on if validation passed.
+ *
+ *
+ * @author Mike Mirzayanov
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Action {
+ /**
+ * @return Action name.
+ */
+ String value() default "";
+
+ /**
+ * @return HTTP method to be handled by the action.
+ */
+ HttpMethod[] method() default {HttpMethod.GET};
+}
diff --git a/code/src/main/java/org/nocturne/annotation/Invalid.java b/code/src/main/java/org/nocturne/annotation/Invalid.java
index 7421c0f..0bbe05f 100644
--- a/code/src/main/java/org/nocturne/annotation/Invalid.java
+++ b/code/src/main/java/org/nocturne/annotation/Invalid.java
@@ -1,25 +1,25 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Set it before page method which should be executed for specified
- * action if validation fails. Action method will no be executed
- * in this case.
- *
- * @author Mike Mirzayanov
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface Invalid {
- /**
- * @return Action name.
- */
- String value() default "";
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Set it before page method which should be executed for specified
+ * action if validation fails. Action method will no be executed
+ * in this case.
+ *
+ * @author Mike Mirzayanov
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Invalid {
+ /**
+ * @return Action name.
+ */
+ String value() default "";
+}
diff --git a/code/src/main/java/org/nocturne/annotation/Name.java b/code/src/main/java/org/nocturne/annotation/Name.java
index caf14bc..2090cd4 100644
--- a/code/src/main/java/org/nocturne/annotation/Name.java
+++ b/code/src/main/java/org/nocturne/annotation/Name.java
@@ -1,30 +1,30 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- *
- * You can specify page name and later easily get links
- * using it.
- *
- *
- * If you don't specify name for page, the
- * page name is equals to page.getClass().getSimpleName().
- *
- *
- * @author Mike Mirzayanov
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface Name {
- /**
- * @return Page name (you can use this value as page name to find its link).
- */
- String value();
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * You can specify page name and later easily get links
+ * using it.
+ *
+ *
+ * If you don't specify name for page, the
+ * page name is equals to page.getClass().getSimpleName().
+ *
+ *
+ * @author Mike Mirzayanov
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Name {
+ /**
+ * @return Page name (you can use this value as page name to find its link).
+ */
+ String value();
+}
diff --git a/code/src/main/java/org/nocturne/annotation/Parameter.java b/code/src/main/java/org/nocturne/annotation/Parameter.java
index 7d0fbc2..6c28967 100644
--- a/code/src/main/java/org/nocturne/annotation/Parameter.java
+++ b/code/src/main/java/org/nocturne/annotation/Parameter.java
@@ -1,114 +1,114 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-/**
- *
- * If your component (page or frame) has fields marked with
- * a @Parameter then nocturne will set them automatically before
- * beforeAction phase. Also nocturne takes care about parameters of
- * action/validate/invalid Controller method parameters.
- *
- * Name means name of the parameter
- * as GET or POST parameter or part
- * of overrideParameters in the response of RequestRouter (LinkedRequestRouter places
- * {param} shortcuts in it).
- *
- *
- * @author Mike Mirzayanov
- */
-@Retention(RUNTIME)
-@Target({FIELD, PARAMETER})
-public @interface Parameter {
- /**
- * @return POST or GET parameter name. Don't set it if it is the same with class field name.
- */
- String name() default "";
-
- /**
- * @return What strategy to strip characters to use. Default value is the most strict StripMode.ID.
- */
- StripMode stripMode() default StripMode.ID;
-
- /**
- * How to strip parameter value.
- */
- enum StripMode {
- /**
- * Do not strip any characters.
- */
- NONE {
- @Override
- public String strip(String value) {
- return value;
- }
- },
-
- /**
- * Leave only safe chars: strip slashes, quotes, angle brackets, ampersand and low-code chars. Also makes trim().
- */
- SAFE {
- @Override
- public String strip(String value) {
- if (value != null) {
- char[] chars = value.toCharArray();
- StringBuilder sb = new StringBuilder(chars.length);
- for (char c : chars) {
- if (c != '/' && c != '&' && c != '<' && c != '>' && c != '\\' && c != '\"' && c != '\'' && c >= ' ') {
- sb.append(c);
- }
- }
- return sb.toString().trim();
- } else {
- return value;
- }
- }
- },
-
- /**
- * Leave only chars which can be part of java ID (see Character.isJavaIdentifierPart).
- */
- ID {
- private boolean isValidChar(char c) {
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
- || (c >= '0' && c <= '9') || (c == '_')
- || (c == '-')
- || (c == '.');
- }
-
- @Override
- public String strip(String value) {
- if (value != null) {
- char[] chars = value.toCharArray();
- StringBuilder sb = new StringBuilder(chars.length);
- for (char c : chars) {
- if (isValidChar(c)) {
- sb.append(c);
- }
- }
- return sb.toString();
- } else {
- return value;
- }
- }
- };
-
- /**
- * @param value Value to be stripped.
- * @return Processed value.
- */
- public abstract String strip(String value);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ *
+ * If your component (page or frame) has fields marked with
+ * a @Parameter then nocturne will set them automatically before
+ * beforeAction phase. Also nocturne takes care about parameters of
+ * action/validate/invalid Controller method parameters.
+ *
+ * Name means name of the parameter
+ * as GET or POST parameter or part
+ * of overrideParameters in the response of RequestRouter (LinkedRequestRouter places
+ * {param} shortcuts in it).
+ *
+ *
+ * @author Mike Mirzayanov
+ */
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+public @interface Parameter {
+ /**
+ * @return POST or GET parameter name. Don't set it if it is the same with class field name.
+ */
+ String name() default "";
+
+ /**
+ * @return What strategy to strip characters to use. Default value is the most strict StripMode.ID.
+ */
+ StripMode stripMode() default StripMode.ID;
+
+ /**
+ * How to strip parameter value.
+ */
+ enum StripMode {
+ /**
+ * Do not strip any characters.
+ */
+ NONE {
+ @Override
+ public String strip(String value) {
+ return value;
+ }
+ },
+
+ /**
+ * Leave only safe chars: strip slashes, quotes, angle brackets, ampersand and low-code chars. Also makes trim().
+ */
+ SAFE {
+ @Override
+ public String strip(String value) {
+ if (value != null) {
+ char[] chars = value.toCharArray();
+ StringBuilder sb = new StringBuilder(chars.length);
+ for (char c : chars) {
+ if (c != '/' && c != '&' && c != '<' && c != '>' && c != '\\' && c != '\"' && c != '\'' && c >= ' ') {
+ sb.append(c);
+ }
+ }
+ return sb.toString().trim();
+ } else {
+ return value;
+ }
+ }
+ },
+
+ /**
+ * Leave only chars which can be part of java ID (see Character.isJavaIdentifierPart).
+ */
+ ID {
+ private boolean isValidChar(char c) {
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+ || (c >= '0' && c <= '9') || (c == '_')
+ || (c == '-')
+ || (c == '.');
+ }
+
+ @Override
+ public String strip(String value) {
+ if (value != null) {
+ char[] chars = value.toCharArray();
+ StringBuilder sb = new StringBuilder(chars.length);
+ for (char c : chars) {
+ if (isValidChar(c)) {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ } else {
+ return value;
+ }
+ }
+ };
+
+ /**
+ * @param value Value to be stripped.
+ * @return Processed value.
+ */
+ public abstract String strip(String value);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/annotation/Validate.java b/code/src/main/java/org/nocturne/annotation/Validate.java
index 9659965..df0ec42 100644
--- a/code/src/main/java/org/nocturne/annotation/Validate.java
+++ b/code/src/main/java/org/nocturne/annotation/Validate.java
@@ -1,28 +1,28 @@
-package org.nocturne.annotation;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- *
- * Set it before page method which should be executed on specified
- * action as validation method. The method should return boolean and
- * the value will define future workflow.
- *
- *
- * Typically validation methods has the line "return runValidation();" as
- * the last line.
- *
+ * Set it before page method which should be executed on specified
+ * action as validation method. The method should return boolean and
+ * the value will define future workflow.
+ *
+ *
+ * Typically validation methods has the line "return runValidation();" as
+ * the last line.
+ *
- * Use implementation of this interface in your pages or frames
- * if you want them to be cached.
- *
- *
- * Method #intercept will be called after Component.prepareForAction
- * and if it returns non-null string it will be used as parsed result instead
- * of typical life-cycle, i.e. methods initializeAction(), Events.fireBeforeAction(this),
- * ..., Events.fireAfterAction(this), finalizeAction()) will not be called.
- *
- *
- * But if it returns null, the typical life-cycle will be used and parsed component
- * will be passed as a result to #postprocess().
- *
+ * Use implementation of this interface in your pages or frames
+ * if you want them to be cached.
+ *
+ *
+ * Method #intercept will be called after Component.prepareForAction
+ * and if it returns non-null string it will be used as parsed result instead
+ * of typical life-cycle, i.e. methods initializeAction(), Events.fireBeforeAction(this),
+ * ..., Events.fireAfterAction(this), finalizeAction()) will not be called.
+ *
+ *
+ * But if it returns null, the typical life-cycle will be used and parsed component
+ * will be passed as a result to #postprocess().
+ *
- * Simple implementation of Captions interface. Uses properties files to store values.
- *
- *
- * In the development mode it try to locate them in the directory
- * ApplicationContext.getInstance().getDebugCaptionsDir() (see nocturne.debug-captions-dir).
- * It modifies all of them if finds new caption shortcut and saves values nocturne.null except
- * for default language which will have value equals to shortcut.
- *
- *
- * In the production mode it just read them exactly once (on startup) and doesn't save them.
- *
- *
- * @author Mike Mirzayanov
- */
-@Singleton
-public class CaptionsImpl implements Captions {
- private static final Logger logger = Logger.getLogger(CaptionsImpl.class);
-
- private static final Pattern CAPTIONS_FILE_PATTERN = Pattern.compile("captions_[\\w]{2}\\.properties");
-
- /**
- * Stores properties per language.
- */
- private final Map propertiesMap = new Hashtable<>();
-
- /**
- * Magic value to store empty value.
- */
- private static final String NULL = "nocturne.null";
-
- /**
- * Constructs new CaptionsImpl.
- */
- public CaptionsImpl() {
- // Load properties on startup.
- loadProperties();
- }
-
- @Override
- public String find(String shortcut, Object... args) {
- return find(ApplicationContext.getInstance().getLocale(), shortcut, args);
- }
-
- @Override
- public String find(Locale locale, String shortcut, Object... args) {
- // Default locale. It is possible equals with current "locale".
- Locale defaultLocale = ApplicationContext.getInstance().getDefaultLocale();
-
- // Current language.
- String language = locale.getLanguage();
-
- // If doesn't contain locale properties?
- if (!propertiesMap.containsKey(language)) {
- // Add empty properties.
- propertiesMap.put(language, new Properties());
- }
-
- // It exists anyway.
- Properties properties = propertiesMap.get(language);
-
- // It can be absent.
- String value = properties.getProperty(shortcut);
-
- // No such value?
- if (value == null || value.equals(NULL)) {
- // Is it NOT default locale?
- if (defaultLocale == locale) {
- // Set default.
- properties.setProperty(shortcut, shortcut);
- }
- // Use default locale to find value.
- value = find(defaultLocale, shortcut, args);
- // Save all properties.
- if (ApplicationContext.getInstance().isDebug()) {
- saveProperties();
- }
- }
-
- if (args.length > 0) {
- return MessageFormat.format(value, args);
- } else {
- return value;
- }
- }
-
- /**
- * Synchronizes all the properties and saves them.
- */
- private void saveProperties() {
- // Find all possible keys.
- Set keys = new TreeSet<>();
- for (Map.Entry entry : propertiesMap.entrySet()) {
- Set
- *
- * Do not throw this exception directly, but use methods like abort
- * abortWithRedirect().
- *
+ * Application will throw this exception on abort of execution
+ * (usually on redirect).
+ *
+ *
+ * Do not throw this exception directly, but use methods like abort
+ * abortWithRedirect().
+ *
+ *
+ * @author Mike Mirzayanov
+ */
+public class AbortException extends RuntimeException {
+ @Nullable
+ private final String redirectionTarget;
+
+ /**
+ * @param message Abort information message (not used).
+ */
+ public AbortException(String message) {
+ this(message, null);
+ }
+
+ /**
+ * @param message Abort information message (not used).
+ * @param redirectionTarget Target URL or {@code null}.
+ */
+ public AbortException(String message, @Nullable String redirectionTarget) {
+ super(message);
+ this.redirectionTarget = redirectionTarget;
+ }
+
+ @Nullable
+ public String getRedirectionTarget() {
+ return redirectionTarget;
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/ConfigurationException.java b/code/src/main/java/org/nocturne/exception/ConfigurationException.java
index 4a527ba..3aae822 100644
--- a/code/src/main/java/org/nocturne/exception/ConfigurationException.java
+++ b/code/src/main/java/org/nocturne/exception/ConfigurationException.java
@@ -1,26 +1,26 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * On illegal application configuration.
- *
- * @author Mike Mirzayanov
- */
-public class ConfigurationException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public ConfigurationException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public ConfigurationException(String message, Throwable cause) {
- super(message, cause);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * On illegal application configuration.
+ *
+ * @author Mike Mirzayanov
+ */
+public class ConfigurationException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public ConfigurationException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public ConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/FreemarkerException.java b/code/src/main/java/org/nocturne/exception/FreemarkerException.java
index 722ed12..baf0b94 100644
--- a/code/src/main/java/org/nocturne/exception/FreemarkerException.java
+++ b/code/src/main/java/org/nocturne/exception/FreemarkerException.java
@@ -1,26 +1,26 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * On illegal freemarker configuration or state.
- *
- * @author Mike Mirzayanov
- */
-public class FreemarkerException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public FreemarkerException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public FreemarkerException(String message, Throwable cause) {
- super(message, cause);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * On illegal freemarker configuration or state.
+ *
+ * @author Mike Mirzayanov
+ */
+public class FreemarkerException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public FreemarkerException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public FreemarkerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/IncorrectLogicException.java b/code/src/main/java/org/nocturne/exception/IncorrectLogicException.java
index e5c891a..2037fef 100644
--- a/code/src/main/java/org/nocturne/exception/IncorrectLogicException.java
+++ b/code/src/main/java/org/nocturne/exception/IncorrectLogicException.java
@@ -1,28 +1,28 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * In case of incorrect logic (bugs) in web-application (client) code.
- * It is throwed if nocturne found broken condition and it means a error in
- * your code.
- *
- * @author Mike Mirzayanov
- */
-public class IncorrectLogicException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public IncorrectLogicException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public IncorrectLogicException(String message, Throwable cause) {
- super(message, cause);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * In case of incorrect logic (bugs) in web-application (client) code.
+ * It is throwed if nocturne found broken condition and it means a error in
+ * your code.
+ *
+ * @author Mike Mirzayanov
+ */
+public class IncorrectLogicException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public IncorrectLogicException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public IncorrectLogicException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/InterruptException.java b/code/src/main/java/org/nocturne/exception/InterruptException.java
index 47ffb9c..66828c2 100644
--- a/code/src/main/java/org/nocturne/exception/InterruptException.java
+++ b/code/src/main/java/org/nocturne/exception/InterruptException.java
@@ -1,26 +1,26 @@
-package org.nocturne.exception;
-
-/**
- * Use it to interrupt action method of Component. It differs from AbortException
- * that the component will be rendered using the template (if no skipTemplate() was called).
- *
- * It is preferred to use interrupt() method instead of throwing it manually.
- *
- * @author Mike Mirzayanov
- */
-public class InterruptException extends RuntimeException {
- public InterruptException() {
- }
-
- public InterruptException(String message) {
- super(message);
- }
-
- public InterruptException(String message, Throwable cause) {
- super(message, cause);
- }
-
- public InterruptException(Throwable cause) {
- super(cause);
- }
-}
+package org.nocturne.exception;
+
+/**
+ * Use it to interrupt action method of Component. It differs from AbortException
+ * that the component will be rendered using the template (if no skipTemplate() was called).
+ *
+ * It is preferred to use interrupt() method instead of throwing it manually.
+ *
+ * @author Mike Mirzayanov
+ */
+public class InterruptException extends RuntimeException {
+ public InterruptException() {
+ }
+
+ public InterruptException(String message) {
+ super(message);
+ }
+
+ public InterruptException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InterruptException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/ModuleInitializationException.java b/code/src/main/java/org/nocturne/exception/ModuleInitializationException.java
index 8706772..0ac8227 100644
--- a/code/src/main/java/org/nocturne/exception/ModuleInitializationException.java
+++ b/code/src/main/java/org/nocturne/exception/ModuleInitializationException.java
@@ -1,27 +1,27 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * If nocturne can't initialize module. Possibly, the module
- * configuration has errors.
- *
- * @author Mike Mirzayanov
- */
-public class ModuleInitializationException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public ModuleInitializationException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public ModuleInitializationException(String message, Throwable cause) {
- super(message, cause);
- }
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * If nocturne can't initialize module. Possibly, the module
+ * configuration has errors.
+ *
+ * @author Mike Mirzayanov
+ */
+public class ModuleInitializationException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public ModuleInitializationException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public ModuleInitializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
\ No newline at end of file
diff --git a/code/src/main/java/org/nocturne/exception/NocturneException.java b/code/src/main/java/org/nocturne/exception/NocturneException.java
index 3cf85d6..9ee1049 100644
--- a/code/src/main/java/org/nocturne/exception/NocturneException.java
+++ b/code/src/main/java/org/nocturne/exception/NocturneException.java
@@ -1,26 +1,26 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * Nocturne fails. I hope you will not see it.
- *
- * @author Mike Mirzayanov
- */
-public class NocturneException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public NocturneException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public NocturneException(String message, Throwable cause) {
- super(message, cause);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * Nocturne fails. I hope you will not see it.
+ *
+ * @author Mike Mirzayanov
+ */
+public class NocturneException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public NocturneException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public NocturneException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/ReflectionException.java b/code/src/main/java/org/nocturne/exception/ReflectionException.java
index da68a11..a927407 100644
--- a/code/src/main/java/org/nocturne/exception/ReflectionException.java
+++ b/code/src/main/java/org/nocturne/exception/ReflectionException.java
@@ -1,27 +1,27 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * If can't invoke operation via reflection. Nocturne uses reflection only
- * in the debug mode.
- *
- * @author Mike Mirzayanov
- */
-public class ReflectionException extends Exception {
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public ReflectionException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * @param message Error message.
- */
- public ReflectionException(String message) {
- super(message);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * If can't invoke operation via reflection. Nocturne uses reflection only
+ * in the debug mode.
+ *
+ * @author Mike Mirzayanov
+ */
+public class ReflectionException extends Exception {
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public ReflectionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * @param message Error message.
+ */
+ public ReflectionException(String message) {
+ super(message);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/ServletException.java b/code/src/main/java/org/nocturne/exception/ServletException.java
index f5f2ce2..51f8343 100644
--- a/code/src/main/java/org/nocturne/exception/ServletException.java
+++ b/code/src/main/java/org/nocturne/exception/ServletException.java
@@ -1,27 +1,27 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * IOExceptions around servlets are wrapped by this exception.
- * And something other servlet-like exceptions are wrapped by it.
- *
- * @author Mike Mirzayanov
- */
-public class ServletException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public ServletException(String message) {
- super(message);
- }
-
- /**
- * @param message Error message.
- * @param cause Cause.
- */
- public ServletException(String message, Throwable cause) {
- super(message, cause);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * IOExceptions around servlets are wrapped by this exception.
+ * And something other servlet-like exceptions are wrapped by it.
+ *
+ * @author Mike Mirzayanov
+ */
+public class ServletException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public ServletException(String message) {
+ super(message);
+ }
+
+ /**
+ * @param message Error message.
+ * @param cause Cause.
+ */
+ public ServletException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/exception/SessionInvalidatedException.java b/code/src/main/java/org/nocturne/exception/SessionInvalidatedException.java
index 09be17a..0347300 100644
--- a/code/src/main/java/org/nocturne/exception/SessionInvalidatedException.java
+++ b/code/src/main/java/org/nocturne/exception/SessionInvalidatedException.java
@@ -1,13 +1,13 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.exception;
-
-/**
- * Tried to access session variable, but session has been
- * invalidated.
- *
- * @author Mike Mirzayanov
- */
-public class SessionInvalidatedException extends RuntimeException {
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.exception;
+
+/**
+ * Tried to access session variable, but session has been
+ * invalidated.
+ *
+ * @author Mike Mirzayanov
+ */
+public class SessionInvalidatedException extends RuntimeException {
+}
diff --git a/code/src/main/java/org/nocturne/geoip/GeoIpUtil.java b/code/src/main/java/org/nocturne/geoip/GeoIpUtil.java
index 8bdd11f..023ec4f 100644
--- a/code/src/main/java/org/nocturne/geoip/GeoIpUtil.java
+++ b/code/src/main/java/org/nocturne/geoip/GeoIpUtil.java
@@ -1,124 +1,124 @@
-package org.nocturne.geoip;
-
-import com.maxmind.db.CHMCache;
-import com.maxmind.db.Reader;
-import com.maxmind.geoip2.DatabaseReader;
-import com.maxmind.geoip2.exception.GeoIp2Exception;
-import com.maxmind.geoip2.model.CityResponse;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Logger;
-import org.nocturne.util.StringUtil;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.servlet.http.HttpServletRequest;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetAddress;
-import java.nio.file.Files;
-import java.util.*;
-
-/**
- * @author Mike Mirzayanov (mirzayanovmr@gmail.com)
- */
-@SuppressWarnings("WeakerAccess")
-public final class GeoIpUtil {
- private static final Logger logger = Logger.getLogger(GeoIpUtil.class);
-
- private static final DatabaseReader COUNTRY_DETECTION_SERVICE;
- private static final DatabaseReader CITY_DETECTION_SERVICE;
-
- private GeoIpUtil() {
- throw new UnsupportedOperationException();
- }
-
- /**
- * @param ip IPv4 or IPv6 ip-address.
- * @return The ISO two-letter country code of country or "--". Example: RU.
- */
- @Nonnull
- public static String getCountryCodeByIp(@Nonnull String ip) {
- try {
- return COUNTRY_DETECTION_SERVICE.country(InetAddress.getByName(ip)).getCountry().getIsoCode();
- } catch (IOException | GeoIp2Exception | RuntimeException ignored) {
- return "--";
- }
- }
-
- @Nullable
- public static String getCityByIp(@Nonnull String ip) {
- try {
- CityResponse cityResponse = CITY_DETECTION_SERVICE.city(InetAddress.getByName(ip));
- if (cityResponse.getCity().getName() == null) {
- return cityResponse.getCountry().getName();
- } else {
- return cityResponse.getCountry().getName() + ", " + cityResponse.getCity().getName();
- }
- } catch (IOException | GeoIp2Exception | RuntimeException ignored) {
- return null;
- }
- }
-
- @Nonnull
- public static Map getCityByIp(@Nonnull Collection ips) {
- Map result = new HashMap<>();
- for (String ip : ips) {
- String city = getCityByIp(ip);
- if (city != null) {
- result.put(ip, city);
- }
- }
- return result;
- }
-
- /**
- * @param httpServletRequest Http request.
- * @return The ISO two-letter country code of country or "--". Example: RU.
- */
- @Nonnull
- public static String getCountryCode(@Nonnull HttpServletRequest httpServletRequest) {
- String ip = StringUtil.trimToNull(httpServletRequest.getHeader("X-Real-IP"));
- if (ip != null) {
- return getCountryCodeByIp(ip);
- }
-
- ip = StringUtil.trimToNull(httpServletRequest.getRemoteAddr());
- if (ip != null) {
- return getCountryCodeByIp(ip);
- }
-
- return "--";
- }
-
- static {
- String countryResourcePath = "/org/nocturne/geoip2/GeoLite2-Country.mmdb";
-
- try {
- COUNTRY_DETECTION_SERVICE = new DatabaseReader.Builder(GeoIpUtil.class.getResourceAsStream(
- countryResourcePath)).withCache(new CHMCache()).fileMode(Reader.FileMode.MEMORY).build();
- } catch (IOException e) {
- throw new RuntimeException("Can't read resource '" + countryResourcePath + "'.", e);
- }
-
- DatabaseReader cityDatabaseReader = null;
- List citiesPaths = Arrays.asList("/srv/app/GeoLite2-City.mmdb", "C:/Temp/GeoLite2-City.mmdb");
- for (String citiesPath : citiesPaths) {
- if (cityDatabaseReader == null) {
- try (InputStream cityInputStream = Files.newInputStream(new File(citiesPath).toPath())) {
- cityDatabaseReader = new DatabaseReader.Builder(cityInputStream)
- .withCache(new CHMCache()).fileMode(Reader.FileMode.MEMORY).build();
- logger.info("GeoLite2-City loaded from '" + citiesPath + "'.");
- } catch (Exception e) {
- logger.info("Can't find \"" + citiesPath + "\".");
- }
- }
- }
-
- if (cityDatabaseReader == null) {
- logger.warn("Can't find GeoLite2-City.mmdb in paths: " + StringUtils.join(citiesPaths, ", ") + ".");
- }
-
- CITY_DETECTION_SERVICE = cityDatabaseReader;
- }
-}
+package org.nocturne.geoip;
+
+import com.maxmind.db.CHMCache;
+import com.maxmind.db.Reader;
+import com.maxmind.geoip2.DatabaseReader;
+import com.maxmind.geoip2.exception.GeoIp2Exception;
+import com.maxmind.geoip2.model.CityResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+import org.nocturne.util.StringUtil;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.util.*;
+
+/**
+ * @author Mike Mirzayanov (mirzayanovmr@gmail.com)
+ */
+@SuppressWarnings("WeakerAccess")
+public final class GeoIpUtil {
+ private static final Logger logger = Logger.getLogger(GeoIpUtil.class);
+
+ private static final DatabaseReader COUNTRY_DETECTION_SERVICE;
+ private static final DatabaseReader CITY_DETECTION_SERVICE;
+
+ private GeoIpUtil() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @param ip IPv4 or IPv6 ip-address.
+ * @return The ISO two-letter country code of country or "--". Example: RU.
+ */
+ @Nonnull
+ public static String getCountryCodeByIp(@Nonnull String ip) {
+ try {
+ return COUNTRY_DETECTION_SERVICE.country(InetAddress.getByName(ip)).getCountry().getIsoCode();
+ } catch (IOException | GeoIp2Exception | RuntimeException ignored) {
+ return "--";
+ }
+ }
+
+ @Nullable
+ public static String getCityByIp(@Nonnull String ip) {
+ try {
+ CityResponse cityResponse = CITY_DETECTION_SERVICE.city(InetAddress.getByName(ip));
+ if (cityResponse.getCity().getName() == null) {
+ return cityResponse.getCountry().getName();
+ } else {
+ return cityResponse.getCountry().getName() + ", " + cityResponse.getCity().getName();
+ }
+ } catch (IOException | GeoIp2Exception | RuntimeException ignored) {
+ return null;
+ }
+ }
+
+ @Nonnull
+ public static Map getCityByIp(@Nonnull Collection ips) {
+ Map result = new HashMap<>();
+ for (String ip : ips) {
+ String city = getCityByIp(ip);
+ if (city != null) {
+ result.put(ip, city);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @param httpServletRequest Http request.
+ * @return The ISO two-letter country code of country or "--". Example: RU.
+ */
+ @Nonnull
+ public static String getCountryCode(@Nonnull HttpServletRequest httpServletRequest) {
+ String ip = StringUtil.trimToNull(httpServletRequest.getHeader("X-Real-IP"));
+ if (ip != null) {
+ return getCountryCodeByIp(ip);
+ }
+
+ ip = StringUtil.trimToNull(httpServletRequest.getRemoteAddr());
+ if (ip != null) {
+ return getCountryCodeByIp(ip);
+ }
+
+ return "--";
+ }
+
+ static {
+ String countryResourcePath = "/org/nocturne/geoip2/GeoLite2-Country.mmdb";
+
+ try {
+ COUNTRY_DETECTION_SERVICE = new DatabaseReader.Builder(GeoIpUtil.class.getResourceAsStream(
+ countryResourcePath)).withCache(new CHMCache()).fileMode(Reader.FileMode.MEMORY).build();
+ } catch (IOException e) {
+ throw new RuntimeException("Can't read resource '" + countryResourcePath + "'.", e);
+ }
+
+ DatabaseReader cityDatabaseReader = null;
+ List citiesPaths = Arrays.asList("/srv/app/GeoLite2-City.mmdb", "C:/Temp/GeoLite2-City.mmdb");
+ for (String citiesPath : citiesPaths) {
+ if (cityDatabaseReader == null) {
+ try (InputStream cityInputStream = Files.newInputStream(new File(citiesPath).toPath())) {
+ cityDatabaseReader = new DatabaseReader.Builder(cityInputStream)
+ .withCache(new CHMCache()).fileMode(Reader.FileMode.MEMORY).build();
+ logger.info("GeoLite2-City loaded from '" + citiesPath + "'.");
+ } catch (Exception e) {
+ logger.info("Can't find \"" + citiesPath + "\".");
+ }
+ }
+ }
+
+ if (cityDatabaseReader == null) {
+ logger.warn("Can't find GeoLite2-City.mmdb in paths: " + StringUtils.join(citiesPaths, ", ") + ".");
+ }
+
+ CITY_DETECTION_SERVICE = cityDatabaseReader;
+ }
+}
diff --git a/code/src/main/java/org/nocturne/link/Link.java b/code/src/main/java/org/nocturne/link/Link.java
index 6adaed9..39fbeff 100644
--- a/code/src/main/java/org/nocturne/link/Link.java
+++ b/code/src/main/java/org/nocturne/link/Link.java
@@ -1,103 +1,103 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.link;
-
-import com.google.common.base.Strings;
-
-import javax.annotation.Nonnull;
-import java.lang.annotation.*;
-
-/**
- * Use it to specify link pattern for page.
- *
- * @author Mike Mirzayanov
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface Link {
- /**
- * @return
- * Use ";" to separate links. Do not use slash as a first character.
- *
- *
- * Example:
- * - "user/{login};profile" - first link contains 'login' parameter, second link contains no parameters;
- * - "user/{login(!blank,alphanumeric):!admin}" - link contains 'login' parameter with restrictions
- * (should not be blank and should contain only letters and digits, also should not be equal to 'admin');
- * - "book/{bookId(long,positive)}" - link contains 'bookId' parameter with restrictions (should be a
- * positive long integer);
- * - "action/{action:purchase,sell,!action}" - link contains 'action' parameter with restrictions (should
- * be either equal to 'purchase' or 'sell' and should not be equal to 'action').
- *
- */
- String value();
-
- /**
- * @return Link name: use it together with getRequest().getAttribute("nocturne.current-page-link").
- */
- String name() default "";
-
- /**
- * @return Default action for page, it will be invoked if no action specified as request parameter.
- */
- String action() default "";
-
- /**
- * @return You can mark link usages with some classes. For example, it
- * could be menu items or layout types.
- */
- Class extends Type>[] types() default {};
-
- /**
- * @return List of interceptors to skip.
- */
- String[] skipInterceptors() default {};
-
- /**
- * Marker interface for type of link.
- */
- interface Type {
- }
-
- class Builder {
- public static Link newLink(@Nonnull String value, @Nonnull String name,
- @Nonnull String action, @Nonnull Class extends Type>[] types) {
- return new Link() {
- @Override
- public String value() {
- return value;
- }
-
- @Override
- public String name() {
- return Strings.nullToEmpty(name);
- }
-
- @Override
- public String action() {
- return Strings.nullToEmpty(action);
- }
-
- @Override
- public Class extends Type>[] types() {
- return types;
- }
-
- @Override
- public Class extends Annotation> annotationType() {
- return Link.class;
- }
-
- @Override
- public String[] skipInterceptors() {
- return new String[0];
- }
- };
- }
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.link;
+
+import com.google.common.base.Strings;
+
+import javax.annotation.Nonnull;
+import java.lang.annotation.*;
+
+/**
+ * Use it to specify link pattern for page.
+ *
+ * @author Mike Mirzayanov
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Link {
+ /**
+ * @return
+ * Use ";" to separate links. Do not use slash as a first character.
+ *
+ *
+ * Example:
+ * - "user/{login};profile" - first link contains 'login' parameter, second link contains no parameters;
+ * - "user/{login(!blank,alphanumeric):!admin}" - link contains 'login' parameter with restrictions
+ * (should not be blank and should contain only letters and digits, also should not be equal to 'admin');
+ * - "book/{bookId(long,positive)}" - link contains 'bookId' parameter with restrictions (should be a
+ * positive long integer);
+ * - "action/{action:purchase,sell,!action}" - link contains 'action' parameter with restrictions (should
+ * be either equal to 'purchase' or 'sell' and should not be equal to 'action').
+ *
+ */
+ String value();
+
+ /**
+ * @return Link name: use it together with getRequest().getAttribute("nocturne.current-page-link").
+ */
+ String name() default "";
+
+ /**
+ * @return Default action for page, it will be invoked if no action specified as request parameter.
+ */
+ String action() default "";
+
+ /**
+ * @return You can mark link usages with some classes. For example, it
+ * could be menu items or layout types.
+ */
+ Class extends Type>[] types() default {};
+
+ /**
+ * @return List of interceptors to skip.
+ */
+ String[] skipInterceptors() default {};
+
+ /**
+ * Marker interface for type of link.
+ */
+ interface Type {
+ }
+
+ class Builder {
+ public static Link newLink(@Nonnull String value, @Nonnull String name,
+ @Nonnull String action, @Nonnull Class extends Type>[] types) {
+ return new Link() {
+ @Override
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public String name() {
+ return Strings.nullToEmpty(name);
+ }
+
+ @Override
+ public String action() {
+ return Strings.nullToEmpty(action);
+ }
+
+ @Override
+ public Class extends Type>[] types() {
+ return types;
+ }
+
+ @Override
+ public Class extends Annotation> annotationType() {
+ return Link.class;
+ }
+
+ @Override
+ public String[] skipInterceptors() {
+ return new String[0];
+ }
+ };
+ }
+ }
+}
diff --git a/code/src/main/java/org/nocturne/link/LinkDirective.java b/code/src/main/java/org/nocturne/link/LinkDirective.java
index addfb4b..951b5ff 100644
--- a/code/src/main/java/org/nocturne/link/LinkDirective.java
+++ b/code/src/main/java/org/nocturne/link/LinkDirective.java
@@ -1,66 +1,66 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.link;
-
-import freemarker.core.Environment;
-import freemarker.template.*;
-
-import java.io.IOException;
-import java.util.Map;
-
-/**
- * Generates page link.
- *
- * @author Mike Mirzayanov
- */
-@SuppressWarnings("Singleton")
-public class LinkDirective implements TemplateDirectiveModel {
- private static final LinkDirective INSTANCE = new LinkDirective();
-
- private LinkDirective() {
- // No operations.
- }
-
- @Override
- public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
- throws TemplateException, IOException {
- if (!params.containsKey("name")) {
- throw new TemplateModelException("LinkDirective directive expects name parameter.");
- }
-
- String name = params.remove("name").toString();
-
- if (loopVars.length != 0) {
- throw new TemplateModelException("LinkDirective directive doesn't allow loop variables.");
- }
-
- Object linkNameValue = params.remove("linkName");
- String linkName = linkNameValue == null ? null : linkNameValue.toString();
-
- if (body == null) {
- if (params.containsKey("value")) {
- String value = params.get("value").toString();
- params.remove("value");
- String a = String.format("%s", getLink(params, name, linkName), value);
- env.getOut().write(a);
- } else {
- env.getOut().write(getLink(params, name, linkName));
- }
- } else {
- throw new TemplateModelException("Body is not expected for LinkDirective directive.");
- }
- }
-
- @SuppressWarnings("unchecked")
- private static String getLink(Map params, String name, String linkName) {
- return linkName == null ? Links.getLinkByMap(name, params) : Links.getLinkByMap(name, linkName, params);
- }
-
- /**
- * @return Returns the only directive instance.
- */
- public static LinkDirective getInstance() {
- return INSTANCE;
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.link;
+
+import freemarker.core.Environment;
+import freemarker.template.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Generates page link.
+ *
+ * @author Mike Mirzayanov
+ */
+@SuppressWarnings("Singleton")
+public class LinkDirective implements TemplateDirectiveModel {
+ private static final LinkDirective INSTANCE = new LinkDirective();
+
+ private LinkDirective() {
+ // No operations.
+ }
+
+ @Override
+ public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+ throws TemplateException, IOException {
+ if (!params.containsKey("name")) {
+ throw new TemplateModelException("LinkDirective directive expects name parameter.");
+ }
+
+ String name = params.remove("name").toString();
+
+ if (loopVars.length != 0) {
+ throw new TemplateModelException("LinkDirective directive doesn't allow loop variables.");
+ }
+
+ Object linkNameValue = params.remove("linkName");
+ String linkName = linkNameValue == null ? null : linkNameValue.toString();
+
+ if (body == null) {
+ if (params.containsKey("value")) {
+ String value = params.get("value").toString();
+ params.remove("value");
+ String a = String.format("%s", getLink(params, name, linkName), value);
+ env.getOut().write(a);
+ } else {
+ env.getOut().write(getLink(params, name, linkName));
+ }
+ } else {
+ throw new TemplateModelException("Body is not expected for LinkDirective directive.");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String getLink(Map params, String name, String linkName) {
+ return linkName == null ? Links.getLinkByMap(name, params) : Links.getLinkByMap(name, linkName, params);
+ }
+
+ /**
+ * @return Returns the only directive instance.
+ */
+ public static LinkDirective getInstance() {
+ return INSTANCE;
+ }
+}
diff --git a/code/src/main/java/org/nocturne/link/LinkMatchResult.java b/code/src/main/java/org/nocturne/link/LinkMatchResult.java
index f8cd86a..07a045a 100644
--- a/code/src/main/java/org/nocturne/link/LinkMatchResult.java
+++ b/code/src/main/java/org/nocturne/link/LinkMatchResult.java
@@ -1,83 +1,83 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.link;
-
-import org.nocturne.main.Page;
-
-import java.util.Map;
-
-/**
- * It is the result for method Links.match(String). Contains information about
- * matched page and matching itself.
- *
- * @author Mike Mirzayanov
- * @author Andrew Lazarev
- */
-public class LinkMatchResult {
- /**
- * Page matched by given link.
- */
- private final Class extends Page> pageClass;
-
- /**
- * Part of link value, matched pattern.
- */
- private final String pattern;
-
- /**
- * Attributes extracted from given link.
- */
- private final Map attributes;
-
- /**
- * Matched link directive.
- */
- private final Link link;
-
- /**
- * Constructor LinkMatchResult creates a new LinkMatchResult instance.
- *
- * @param pageClass Matched page class.
- * @param pattern Matched pattern (link value).
- * @param attributes attributes extracted from given matching.
- * @param link Matched @Link instance.
- */
- public LinkMatchResult(Class extends Page> pageClass, String pattern, Map attributes, Link link) {
- this.pageClass = pageClass;
- this.pattern = pattern;
- this.attributes = attributes;
- this.link = link;
- }
-
- /**
- * @return Matched page class.
- */
- public Class extends Page> getPageClass() {
- return pageClass;
- }
-
- /**
- * @return Pattern which was matched.
- */
- public String getPattern() {
- return pattern;
- }
-
- /**
- * @return Attributes which was extracted from matching.
- * For example, it pattern="user/{login}" and Links.match()
- * argument is "user/mike"
- * then the returned map will be {@literal {"login"=>"mike"}}.
- */
- public Map getAttributes() {
- return attributes;
- }
-
- /**
- * @return Returns matched link.
- */
- public Link getLink() {
- return link;
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.link;
+
+import org.nocturne.main.Page;
+
+import java.util.Map;
+
+/**
+ * It is the result for method Links.match(String). Contains information about
+ * matched page and matching itself.
+ *
+ * @author Mike Mirzayanov
+ * @author Andrew Lazarev
+ */
+public class LinkMatchResult {
+ /**
+ * Page matched by given link.
+ */
+ private final Class extends Page> pageClass;
+
+ /**
+ * Part of link value, matched pattern.
+ */
+ private final String pattern;
+
+ /**
+ * Attributes extracted from given link.
+ */
+ private final Map attributes;
+
+ /**
+ * Matched link directive.
+ */
+ private final Link link;
+
+ /**
+ * Constructor LinkMatchResult creates a new LinkMatchResult instance.
+ *
+ * @param pageClass Matched page class.
+ * @param pattern Matched pattern (link value).
+ * @param attributes attributes extracted from given matching.
+ * @param link Matched @Link instance.
+ */
+ public LinkMatchResult(Class extends Page> pageClass, String pattern, Map attributes, Link link) {
+ this.pageClass = pageClass;
+ this.pattern = pattern;
+ this.attributes = attributes;
+ this.link = link;
+ }
+
+ /**
+ * @return Matched page class.
+ */
+ public Class extends Page> getPageClass() {
+ return pageClass;
+ }
+
+ /**
+ * @return Pattern which was matched.
+ */
+ public String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * @return Attributes which was extracted from matching.
+ * For example, it pattern="user/{login}" and Links.match()
+ * argument is "user/mike"
+ * then the returned map will be {@literal {"login"=>"mike"}}.
+ */
+ public Map getAttributes() {
+ return attributes;
+ }
+
+ /**
+ * @return Returns matched link.
+ */
+ public Link getLink() {
+ return link;
+ }
+}
diff --git a/code/src/main/java/org/nocturne/link/LinkSet.java b/code/src/main/java/org/nocturne/link/LinkSet.java
index 7732f82..d851828 100644
--- a/code/src/main/java/org/nocturne/link/LinkSet.java
+++ b/code/src/main/java/org/nocturne/link/LinkSet.java
@@ -1,19 +1,19 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.link;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * @author Mike Mirzayanov
- */
-
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
-public @interface LinkSet {
- Link[] value() default {};
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.link;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Mike Mirzayanov
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface LinkSet {
+ Link[] value() default {};
+}
diff --git a/code/src/main/java/org/nocturne/link/Links.java b/code/src/main/java/org/nocturne/link/Links.java
index 78a1ebb..983106f 100644
--- a/code/src/main/java/org/nocturne/link/Links.java
+++ b/code/src/main/java/org/nocturne/link/Links.java
@@ -1,1109 +1,1109 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.link;
-
-import freemarker.template.TemplateModel;
-import freemarker.template.TemplateModelException;
-import freemarker.template.TemplateSequenceModel;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.mutable.MutableBoolean;
-import org.nocturne.annotation.Name;
-import org.nocturne.collection.SingleEntryList;
-import org.nocturne.exception.ConfigurationException;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.main.ApplicationContext;
-import org.nocturne.main.Page;
-import org.nocturne.util.StringUtil;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.io.UnsupportedEncodingException;
-import java.lang.reflect.Array;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- *
- * Handles link pattern methods.
- * Each page should have @Link annotation to specify its link.
- * It can use parameters (templates), like "profile/{handle}".
- *
- *
- * If you want to redirect to SomePage, use abortWithRedirect(Links.getLink(SomePage.class)) or
- * abortWithRedirect(SomePage.class).
- *
- *
- * @author Mike Mirzayanov
- */
-public class Links {
- private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(Links.class);
-
- private static final Lock addLinkLock = new ReentrantLock();
-
- private static final int INTERCEPTOR_MAX_PERMIT_COUNT = 8 * Runtime.getRuntime().availableProcessors();
- private static final Semaphore interceptorSemaphore = new Semaphore(INTERCEPTOR_MAX_PERMIT_COUNT);
- private static final Map interceptorByNameMap = new LinkedHashMap<>();
-
-
- /**
- * Stores maps for each page class. Each map contains single patterns as keys
- * and Link instances as values.
- */
- private static final ConcurrentMap, Map> linksByPage = new ConcurrentHashMap<>();
-
- /**
- * Stores page classes by their names.
- */
- private static final ConcurrentMap> classesByName = new ConcurrentHashMap<>();
-
- /**
- * Stores link sections by links.
- */
- private static final ConcurrentMap> sectionsByLinkText = new ConcurrentHashMap<>();
-
- private static List getLinksViaReflection(Class extends Page> clazz) {
- List result = new ArrayList<>();
- Link link = clazz.getAnnotation(Link.class);
- if (link != null) {
- result.add(link);
- }
- LinkSet linkSet = clazz.getAnnotation(LinkSet.class);
- if (linkSet != null) {
- result.addAll(Arrays.asList(linkSet.value()));
- }
- return result;
- }
-
- /**
- * Use the method to get page link name. Do not use page.getClass().getSimpleName() because of two reasons:
- * - page can have @Name annotation,
- * - page can be actually inherited from expected ConcretePage class because of IoC.
- *
- * @param page Page instance.
- * @return Page link name.
- */
- public static String getLinkName(@Nonnull Page page) {
- return getLinkName(page.getClass());
- }
-
- /**
- * Use the method to get page class link name. Do not use pageClass.getSimpleName() because of two reasons:
- * - page class can have @Name annotation,
- * - page class can be actually inherited from expected ConcretePage class because of IoC.
- *
- * @param pageClass Page class.
- * @return Page link name.
- */
- public static String getLinkName(@Nonnull Class extends Page> pageClass) {
- Class> clazz = pageClass;
-
- while (clazz != null && clazz.getAnnotation(Link.class) == null && clazz.getAnnotation(LinkSet.class) == null) {
- clazz = clazz.getSuperclass();
- }
-
- if (clazz == null) {
- logger.error("Page class should have @Link or @LinkSet annotation, but "
- + pageClass.getName() + " hasn't.");
- throw new NocturneException("Page class should have @Link or @LinkSet annotation, but "
- + pageClass.getName() + " hasn't.");
- }
-
- Name name = clazz.getAnnotation(Name.class);
- if (name == null) {
- return clazz.getSimpleName();
- } else {
- return name.value();
- }
- }
-
- /**
- * @param clazz Page class to be added into Links.
- * After it you can get it's link via getLink, or using @link directive
- * from template. Link may contain template sections, like "profile/{handle}".
- * @param linkSet List of links to be added for class {@code clazz}.
- */
- public static void add(Class extends Page> clazz, List linkSet) {
- addLinkLock.lock();
-
- try {
- String name = getLinkName(clazz);
- if (classesByName.containsKey(name) && !clazz.equals(classesByName.get(name))) {
- logger.error("Can't add page which is not unique by it's name: " + clazz.getName() + '.');
- throw new ConfigurationException("Can't add page which is not unique by it's name: "
- + clazz.getName() + '.');
- }
- classesByName.put(name, clazz);
-
- Map links = getLinksByPageClass(clazz);
- if (links == null) {
- // It is important that used synchronizedMap, because of "synchronized(links) {..}" later in code.
- links = Collections.synchronizedMap(new LinkedHashMap());
- }
-
- for (Link link : linkSet) {
- String[] pageLinks = StringUtil.Patterns.SEMICOLON_PATTERN.split(link.value());
- for (String pageLink : pageLinks) {
- if (!sectionsByLinkText.containsKey(pageLink)) {
- sectionsByLinkText.putIfAbsent(pageLink, parseLinkToLinkSections(pageLink));
- }
-
- for (Map linkMap : linksByPage.values()) {
- if (linkMap.containsKey(pageLink)) {
- logger.error("Page link \"" + pageLink + "\" already registered.");
- throw new ConfigurationException("Page link \"" + pageLink + "\" already registered.");
- }
- }
- if (links.containsKey(pageLink)) {
- logger.error("Page link \"" + pageLink + "\" already registered.");
- throw new ConfigurationException("Page link \"" + pageLink + "\" already registered.");
- }
-
- links.put(pageLink, link);
- }
- }
-
- linksByPage.put(clazz, links);
- } finally {
- addLinkLock.unlock();
- }
- }
-
- /**
- * @param clazz Page class to be added into Links.
- * After it you can get it's link via getLink, or using @link directive
- * from template. Link may contain template sections, like "profile/{handle}".
- */
- public static void add(Class extends Page> clazz) {
- List linkSet = getLinksViaReflection(clazz);
- if (linkSet.isEmpty()) {
- logger.error("Can't find link for page " + clazz.getName() + '.');
- throw new ConfigurationException("Can't find link for page " + clazz.getName() + '.');
- }
-
- add(clazz, linkSet);
- }
-
- /**
- * @param clazz Page class.
- * @param linkName desired {@link Link#name() name} of the link
- * @param params parameters for substitution (for example link "profile/{handle}"
- * may use "handle" key in the map.
- * @return link for page. If there many links for page, returns one of them, which matches better
- * @throws NoSuchLinkException if no such link exists
- */
- @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod"})
- public static String getLinkByMap(Class extends Page> clazz, @Nullable String linkName, Map params) {
- MutableBoolean multiValueParams = new MutableBoolean();
- Map> nonEmptyParams = getNonEmptyParams(params, multiValueParams);
-
- int bestMatchedCount = -1;
- List bestMatchedLinkSections = null;
- Link bestMatchedLink = null;
-
- for (Map.Entry entry : getLinksByPageClass(clazz).entrySet()) {
- if (linkName != null && !linkName.isEmpty() && !linkName.equals(entry.getValue().name())) {
- continue;
- }
-
- List sections = sectionsByLinkText.get(entry.getKey());
- boolean matched = true;
- int matchedCount = 0;
- for (LinkSection section : sections) {
- if (section.isParameter()) {
- ++matchedCount;
- List values = nonEmptyParams.get(section.getParameterName());
- String value = values == null ? null : values.get(0);
- if (value == null || !section.isSuitable(value)) {
- matched = false;
- break;
- }
- }
- }
- if (matched && matchedCount > bestMatchedCount) {
- bestMatchedCount = matchedCount;
- bestMatchedLinkSections = sections;
- bestMatchedLink = entry.getValue();
- }
- }
-
- if (bestMatchedLinkSections == null) {
- if (linkName == null || linkName.isEmpty()) {
- throw new NoSuchLinkException("Can't find link for page " + clazz.getName() + '.');
- } else {
- throw new NoSuchLinkException("Can't find link with name '"
- + linkName + "' for page " + clazz.getName() + '.');
- }
- }
-
- StringBuilder result = new StringBuilder(ApplicationContext.getInstance().getContextPath());
- Set usedKeys = new HashSet<>();
-
- for (LinkSection section : bestMatchedLinkSections) {
- String item;
-
- if (section.isParameter()) {
- usedKeys.add(section.getParameterName());
- item = nonEmptyParams.get(section.getParameterName()).get(0);
- } else {
- item = section.getValue();
- }
-
- result.append('/').append(item);
- }
-
- if (nonEmptyParams.size() > usedKeys.size() || multiValueParams.isTrue()) {
- boolean first = true;
- for (Map.Entry> entry : nonEmptyParams.entrySet()) {
- List values = entry.getValue();
- int valueCount = values.size();
- int startIndex = valueCount;
-
- if (usedKeys.contains(entry.getKey())) {
- if (valueCount > 1) {
- startIndex = 1;
- }
- } else {
- startIndex = 0;
- }
-
- for (int i = startIndex; i < valueCount; ++i) {
- if (first) {
- result.append('?');
- first = false;
- } else {
- result.append('&');
- }
-
- try {
- result.append(entry.getKey()).append('=').append(URLEncoder.encode(values.get(i), StandardCharsets.UTF_8.name()));
- } catch (UnsupportedEncodingException e) {
- // No operations.
- }
- }
- }
- }
-
- String linkResult = result.toString();
-
- interceptorSemaphore.acquireUninterruptibly();
- try {
- for (Map.Entry e : interceptorByNameMap.entrySet()) {
- boolean skip = false;
- for (String skipInterceptor : bestMatchedLink.skipInterceptors()) {
- if (skipInterceptor.equals(e.getKey())) {
- skip = true;
- break;
- }
- }
- if (!skip) {
- linkResult = e.getValue().postprocess(linkResult, clazz, linkName, params);
- }
- }
- } finally {
- interceptorSemaphore.release();
- }
-
- return linkResult;
- }
-
- private static Map> getNonEmptyParams(Map params, MutableBoolean multiValueParams) {
- multiValueParams.setValue(false);
- Map> nonEmptyParams = new LinkedHashMap<>();
-
- for (Map.Entry entry : params.entrySet()) {
- Object value = entry.getValue();
- if (!isMissingValue(value)) {
- List list = toStringList(value);
- int count = list.size();
-
- if (count > 0) {
- nonEmptyParams.put(entry.getKey(), list);
-
- if (count > 1) {
- multiValueParams.setValue(true);
- }
- }
- }
- }
-
- return nonEmptyParams;
- }
-
- @Nonnull
- private static List toStringList(@Nonnull Object value) {
- if (value instanceof TemplateSequenceModel) {
- return toStringList((TemplateSequenceModel) value);
- } else if (value instanceof Collection) {
- return toStringList((Collection) value);
- } else if (value.getClass().isArray()) {
- int count = Array.getLength(value);
- List list = new ArrayList<>(count);
-
- for (int i = 0; i < count; ++i) {
- Object item = Array.get(value, i);
- if (item != null) {
- list.add(item.toString());
- }
- }
-
- return list;
- } else {
- return new SingleEntryList<>(value.toString());
- }
- }
-
- private static List toStringList(@Nonnull TemplateSequenceModel sequence) {
- int count = getSize(sequence);
- List list = new ArrayList<>(count);
-
- for (int i = 0; i < count; ++i) {
- TemplateModel item;
- try {
- item = sequence.get(i);
- } catch (TemplateModelException e) {
- logger.error("Can't get item of Freemarker sequence.", e);
- throw new NocturneException("Can't get item of Freemarker sequence.", e);
- }
-
- if (item != null) {
- list.add(item.toString());
- }
- }
-
- return list;
- }
-
- private static List toStringList(@Nonnull Collection collection) {
- List list = new ArrayList<>(collection.size());
-
- for (Object item : collection) {
- if (item != null) {
- list.add(item.toString());
- }
- }
-
- return list;
- }
-
- private static int getSize(@Nonnull TemplateSequenceModel sequence) {
- try {
- return sequence.size();
- } catch (TemplateModelException e) {
- logger.error("Can't get size of Freemarker sequence.", e);
- throw new NocturneException("Can't get size of Freemarker sequence.", e);
- }
- }
-
- /**
- * @param clazz Page class.
- * @param params parameters for substitution (for example link "profile/{handle}"
- * may use "handle" key in the map.
- * @return Returns link for page. If there many links for page, returns
- * one of them, which matches better. Throws NoSuchLinkException
- * if no such link exists.
- */
- public static String getLinkByMap(Class extends Page> clazz, Map params) {
- return getLinkByMap(clazz, null, params);
- }
-
- /**
- * @param name Page name.
- * @param linkName desired {@link Link#name() name} of the link
- * @param params parameters for substitution (for example link "profile/{handle}"
- * may use "handle" key in the map.
- * @return Returns link for page. If there many links for page, returns
- * one of them, which matches better. Throws NoSuchLinkException
- * if no such link exists.
- */
- public static String getLinkByMap(String name, @Nullable String linkName, Map params) {
- Class extends Page> clazz = classesByName.get(name);
-
- if (clazz == null) {
- logger.error("Can't find link for page " + name + '.');
- throw new NoSuchLinkException("Can't find link for page " + name + '.');
- } else {
- return getLinkByMap(clazz, linkName, params);
- }
- }
-
- /**
- * @param name Page name.
- * @param params parameters for substitution (for example link "profile/{handle}"
- * may use "handle" key in the map.
- * @return Returns link for page. If there many links for page, returns
- * one of them, which matches better. Throws NoSuchLinkException
- * if no such link exists.
- */
- public static String getLinkByMap(String name, Map params) {
- return getLinkByMap(name, null, params);
- }
-
- private static boolean isMissingValue(Object value) {
- if (value == null) {
- return true;
- }
-
- if (value instanceof TemplateSequenceModel) {
- return getSize((TemplateSequenceModel) value) <= 0;
- } else if (value instanceof Collection) {
- return ((Collection) value).isEmpty();
- } else if (value.getClass().isArray()) {
- return Array.getLength(value) <= 0;
- } else {
- return value.toString().isEmpty();
- }
- }
-
- /**
- * @param params Array of values.
- * @return Correspondent map.
- */
- private static Map convertArrayToMap(Object... params) {
- int paramCount = params.length;
-
- if (paramCount == 0) {
- return Collections.emptyMap();
- }
-
- if (paramCount % 2 != 0) {
- logger.error("Params should contain even number of elements.");
- throw new IllegalArgumentException("Params should contain even number of elements.");
- }
-
- Map map = new LinkedHashMap<>();
-
- for (int paramIndex = 0; paramIndex < paramCount; paramIndex += 2) {
- map.put(params[paramIndex].toString(), params[paramIndex + 1]);
- }
-
- return map;
- }
-
- /**
- * @param pageClass Page class.
- * @return link for page. If there many links for page, returns one of them, which matches better
- * @throws NoSuchLinkException if no such link exists
- */
- public static String getLink(Class extends Page> pageClass) {
- return getLinkByMap(pageClass, null, Collections.emptyMap());
- }
-
- /**
- * @param pageClass Page class.
- * @param params Even length sequence of Objects. Even elements mean keys and odd
- * values of parameters map. For example ["handle", "MikeMirzayanov", "topic", 123]
- * means map {@literal ["handle" => "MikeMirzayanov", "topic" => 123]}. Method skips params with null value.
- * @return link for page. If there many links for page, returns one of them, which matches better
- * @throws NoSuchLinkException if no such link exists
- */
- public static String getLink(Class extends Page> pageClass, Object... params) {
- return getLinkByMap(pageClass, null, convertArrayToMap(params));
- }
-
- /**
- * @param name Page name.
- * @param params Even length sequence of Objects. Even elements mean keys and odd
- * values of parameters map. For example ["handle", "MikeMirzayanov", "topic", 123]
- * means map {@literal ["handle" => "MikeMirzayanov", "topic" => 123]}. Method skips params with null value.
- * @return link for page. If there many links for page, returns one of them, which matches better
- * @throws NoSuchLinkException if no such link exists
- */
- public static String getLink(String name, Object... params) {
- return getLinkByMap(name, null, convertArrayToMap(params));
- }
-
- /**
- * @param link Relative link to the page started from "/".
- * For example, "/profile/MikeMirzayanov".
- * @return Result instance or {@code null} if not found.
- */
- public static LinkMatchResult match(String link) {
- // Remove anchor.
- if (link.contains("#")) {
- link = link.substring(0, link.lastIndexOf('#'));
- }
-
- // Remove query string.
- if (link.contains("?")) {
- link = link.substring(0, link.lastIndexOf('?'));
- }
-
- if (!link.startsWith("/")) {
- logger.error("Link \"" + link + "\" doesn't start with '/'.");
- throw new IllegalArgumentException("Link \"" + link + "\" doesn't start with '/'.");
- }
-
- String[] linkTokens = StringUtil.Patterns.SLASH_PATTERN.split(link.substring(1));
-
- for (Map.Entry, Map> listEntry : linksByPage.entrySet()) {
- Map patterns = listEntry.getValue();
- if (patterns == null) {
- continue;
- }
-
- //noinspection SynchronizationOnLocalVariableOrMethodParameter
- synchronized (patterns) {
- for (Map.Entry patternEntry : patterns.entrySet()) {
- String linkText = patternEntry.getKey();
- Map attrs = match(linkTokens, linkText);
-
- if (attrs != null) {
- return new LinkMatchResult(listEntry.getKey(), linkText, attrs, patternEntry.getValue());
- }
- }
- }
- }
-
- return null;
- }
-
- /**
- * @param linkTokens For example, ["profile", "MikeMirzayanov"] for requested link "/profile/MikeMirzayanov".
- * @param linkText Link pattern, like "profile/{handle}".
- * @return Correspondent params map or {@code null} if not matched.
- */
- private static Map match(String[] linkTokens, String linkText) {
- List sections = sectionsByLinkText.get(linkText);
- if (sections == null) {
- logger.error("Can't find sections for linkText=\"" + linkText + "\".");
- throw new NocturneException("Can't find sections for linkText=\"" + linkText + "\".");
- }
-
- int linkTokenCount = linkTokens.length;
-
- if (linkTokenCount == sections.size()) {
- Map attrs = new HashMap<>();
-
- for (int linkTokenIndex = 0; linkTokenIndex < linkTokenCount; ++linkTokenIndex) {
- LinkSection section = sections.get(linkTokenIndex);
-
- if (section.isParameter()) {
- if (!section.isSuitable(linkTokens[linkTokenIndex])) {
- return null;
- }
- attrs.put(section.getParameterName(), linkTokens[linkTokenIndex]);
- } else {
- if (!section.getValue().equals(linkTokens[linkTokenIndex])) {
- return null;
- }
- }
- }
-
- return attrs;
- } else {
- return null;
- }
- }
-
- /**
- * If case of getLink-like methods if they can't find requested link.
- */
- @SuppressWarnings({"DeserializableClassInSecureContext", "UncheckedExceptionClass"})
- public static class NoSuchLinkException extends RuntimeException {
- /**
- * @param message Error message.
- */
- public NoSuchLinkException(String message) {
- super(message);
- }
- }
-
- private static List parseLinkToLinkSections(String linkText) {
- if (linkText == null || linkText.startsWith("/") || linkText.endsWith("/")) {
- logger.error("Page link has illegal format, use links like 'home', 'page/{index}', " +
- "'page/{index(long,positive):1,2,3}', 'section/{name(string,!blank):!a,b,c}'.");
- throw new ConfigurationException("Page link has illegal format, use links like 'home', 'page/{index}', " +
- "'page/{index(long,positive):1,2,3}', 'section/{name(string,!blank):!a,b,c}'."
- );
- }
-
- String[] sections = StringUtil.Patterns.SLASH_PATTERN.split(linkText);
- List linkSections = new ArrayList<>(sections.length);
- for (String section : sections) {
- linkSections.add(new LinkSection(section));
- }
-
- return linkSections;
- }
-
- private static final class LinkSection {
- private final String section;
- private final boolean parameter;
- private final String value;
- private final String parameterName;
- private final List parameterRestrictions;
- private final Set allowedParameterValues;
- private final Set forbiddenParameterValues;
-
- /**
- * @param section Each part of link, i.e. "home" from link "test/home"
- */
- @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod", "OverlyNestedMethod"})
- private LinkSection(String section) {
- section = StringUtil.trim(section);
- this.section = section;
-
- if (section.startsWith("{") && section.endsWith("}")) {
- parameter = true;
- value = null;
-
- String[] parts = StringUtil.Patterns.COLON_PATTERN.split(section.substring(1, section.length() - 1));
- int partCount = parts.length;
-
- if (partCount >= 1 && partCount <= 2) {
- String namePart = StringUtil.trimToNull(parts[0]);
- if (namePart == null) {
- throw getInvalidSectionException(section);
- }
- int namePartLength = namePart.length();
-
- parameterRestrictions = new ArrayList<>(0);
-
- if (namePart.charAt(namePartLength - 1) == ')') {
- int openParenthesisIndex = namePart.indexOf('(');
-
- if (openParenthesisIndex >= 0) {
- parameterName = namePart.substring(0, openParenthesisIndex);
-
- String[] restrictionRules = StringUtil.Patterns.COMMA_PATTERN.split(
- namePart.substring(openParenthesisIndex + 1, namePartLength - 1)
- );
-
- for (String restrictionRule : restrictionRules) {
- if (StringUtil.isEmpty(restrictionRule = StringUtil.trim(restrictionRule))) {
- continue;
- }
-
- parameterRestrictions.add(getParameterRestriction(section, restrictionRule));
- }
- } else {
- parameterName = namePart;
- }
- } else {
- parameterName = namePart;
- }
-
- if (partCount == 1) {
- allowedParameterValues = null;
- forbiddenParameterValues = null;
- } else {
- allowedParameterValues = new HashSet<>();
- forbiddenParameterValues = new HashSet<>();
-
- for (String valueRule : StringUtil.Patterns.COMMA_PATTERN.split(parts[1])) {
- if (StringUtil.isEmpty(valueRule = StringUtil.trim(valueRule))) {
- continue;
- }
-
- if (valueRule.charAt(0) == '!') {
- forbiddenParameterValues.add(valueRule.substring(1));
- } else {
- allowedParameterValues.add(valueRule);
- }
- }
-
- if (!allowedParameterValues.isEmpty()) {
- parameterRestrictions.add(new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return allowedParameterValues.contains(value);
- }
- });
- }
-
- if (!forbiddenParameterValues.isEmpty()) {
- parameterRestrictions.add(new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return !forbiddenParameterValues.contains(value);
- }
- });
- }
- }
- } else {
- throw getInvalidSectionException(section);
- }
- } else {
- parameter = false;
- value = section;
- parameterName = null;
- parameterRestrictions = null;
- allowedParameterValues = null;
- forbiddenParameterValues = null;
- }
- }
-
- public boolean isParameter() {
- return parameter;
- }
-
- public String getValue() {
- ensureValueSection("value");
- return value;
- }
-
- public String getParameterName() {
- ensureParameterSection("parameterName");
- return parameterName;
- }
-
- public List getParameterRestrictions() {
- ensureParameterSection("parameterRestrictions");
- return Collections.unmodifiableList(parameterRestrictions);
- }
-
- public boolean isSuitable(String value) {
- for (ParameterRestriction parameterRestriction : getParameterRestrictions()) {
- if (!parameterRestriction.isSuitable(value)) {
- return false;
- }
- }
-
- return true;
- }
-
- private void ensureValueSection(String fieldName) {
- if (parameter) {
- logger.error("Can't read field '" + fieldName + "' of non-value section '" + section + "'.");
- throw new IllegalStateException(String.format(
- "Can't read field '%s' of non-value section '%s'.", fieldName, section
- ));
- }
- }
-
- private void ensureParameterSection(String fieldName) {
- if (!parameter) {
- logger.error("Can't read field '" + fieldName + "' of non-parameter section '" + section + "'.");
- throw new IllegalStateException(String.format(
- "Can't read field '%s' of non-parameter section '%s'.", fieldName, section
- ));
- }
- }
-
- private static ConfigurationException getInvalidSectionException(String section) {
- return new ConfigurationException("Link section '" + section + "' has invalid format, " +
- "examples of valid formats: 'test', '{userName}', '{id(int):1,2,3}', " +
- "{title(string,!empty):!title}."
- );
- }
-
- private interface ParameterRestriction {
- boolean isSuitable(String value);
- }
-
- private static final class NegatedParameterRestriction implements ParameterRestriction {
- @Nonnull
- private final ParameterRestriction parameterRestriction;
-
- private NegatedParameterRestriction(@Nonnull ParameterRestriction parameterRestriction) {
- this.parameterRestriction = parameterRestriction;
- }
-
- @Override
- public boolean isSuitable(String value) {
- return !parameterRestriction.isSuitable(value);
- }
- }
-
- private static ParameterRestriction getParameterRestriction(String section, String restrictionRule) {
- if (restrictionRule != null && restrictionRule.startsWith("!")) {
- return new NegatedParameterRestriction(internalGetParameterRestriction(
- section, restrictionRule.substring(1)
- ));
- } else {
- return internalGetParameterRestriction(section, restrictionRule);
- }
- }
-
- @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod"})
- @Nonnull
- private static ParameterRestriction internalGetParameterRestriction(String section, String restrictionRule) {
- if ("null".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return value == null;
- }
- };
- } else if ("empty".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return StringUtil.isEmpty(value);
- }
- };
- } else if ("blank".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return StringUtil.isBlank(value);
- }
- };
- } else if ("alpha".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return StringUtils.isAlpha(value);
- }
- };
- } else if ("numeric".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return StringUtils.isNumeric(value);
- }
- };
- } else if ("alphanumeric".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- return StringUtils.isAlphanumeric(value);
- }
- };
- } else if ("byte".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Byte.parseByte(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("short".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Short.parseShort(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("int".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Integer.parseInt(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("long".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Long.parseLong(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("float".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Float.parseFloat(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("double".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- Double.parseDouble(value);
- return true;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("positive".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) > 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("nonpositive".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) <= 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("negative".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) < 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("nonnegative".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) >= 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("zero".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) == 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else if ("nonzero".equalsIgnoreCase(restrictionRule)) {
- return new ParameterRestriction() {
- @Override
- public boolean isSuitable(String value) {
- try {
- return Double.parseDouble(value) != 0.0D;
- } catch (RuntimeException ignored) {
- return false;
- }
- }
- };
- } else {
- logger.error("Link section '" + section + "' contains unsupported parameter restriction '"
- + restrictionRule + "'.");
- throw new ConfigurationException(String.format(
- "Link section '%s' contains unsupported parameter restriction '%s'.",
- section, restrictionRule
- ));
- }
- }
- }
-
- /**
- * Adds interceptor to the Links. Link will be processed by interceptors before return.
- *
- * @param name name of the interceptor to add
- * @param interceptor interceptor to add
- */
- public static void addInterceptor(String name, Interceptor interceptor) {
- ensureInterceptorName(name);
-
- if (interceptor == null) {
- logger.error("Argument 'interceptor' is 'null'.");
- throw new IllegalArgumentException("Argument 'interceptor' is 'null'.");
- }
-
- interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
- try {
- if (interceptorByNameMap.containsKey(name)) {
- logger.error("Interceptor with name '" + name + "' already added.");
- throw new IllegalStateException("Interceptor with name '" + name + "' already added.");
- }
- interceptorByNameMap.put(name, interceptor);
- } finally {
- interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
- }
- }
-
- /**
- * Removes interceptor from the Links.
- *
- * @param name name of the interceptor to remove
- */
- public static void removeInterceptor(String name) {
- ensureInterceptorName(name);
-
- interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
- try {
- interceptorByNameMap.remove(name);
- } finally {
- interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
- }
- }
-
- /**
- * Checks if specified interceptor is already added to the Links.
- *
- * @param name name of the interceptor to check
- * @return {@code true} iff interceptor with specified name is added to the Links
- */
- public static boolean hasInterceptor(String name) {
- ensureInterceptorName(name);
-
- interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
- try {
- return interceptorByNameMap.containsKey(name);
- } finally {
- interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
- }
- }
-
- private static void ensureInterceptorName(String name) {
- if (name == null || name.isEmpty()) {
- logger.error("Argument 'name' is 'null' or empty.");
- throw new IllegalArgumentException("Argument 'name' is 'null' or empty.");
- }
- }
-
- private static Map getLinksByPageClass(Class extends Page> clazz) {
- Map links;
- Class parentClass = clazz;
- while ((links = linksByPage.get(parentClass)) == null && parentClass.getSuperclass() != null) {
- parentClass = parentClass.getSuperclass();
- }
- return links;
- }
-
- /**
- * Custom link processor. You can add interceptor using {@link #addInterceptor(String, Interceptor)} method.
- */
- public interface Interceptor {
- /**
- * This method will be called to postprocess link.
- *
- * @param link link to process
- * @param clazz page class
- * @param linkName {@link Link#name() name} of the link
- * @param params parameters of the link
- * @return processed link
- */
- String postprocess(String link, Class extends Page> clazz, @Nullable String linkName, Map params);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.link;
+
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateSequenceModel;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.mutable.MutableBoolean;
+import org.nocturne.annotation.Name;
+import org.nocturne.collection.SingleEntryList;
+import org.nocturne.exception.ConfigurationException;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.main.ApplicationContext;
+import org.nocturne.main.Page;
+import org.nocturne.util.StringUtil;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ *
+ * Handles link pattern methods.
+ * Each page should have @Link annotation to specify its link.
+ * It can use parameters (templates), like "profile/{handle}".
+ *
+ *
+ * If you want to redirect to SomePage, use abortWithRedirect(Links.getLink(SomePage.class)) or
+ * abortWithRedirect(SomePage.class).
+ *
+ *
+ * @author Mike Mirzayanov
+ */
+public class Links {
+ private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(Links.class);
+
+ private static final Lock addLinkLock = new ReentrantLock();
+
+ private static final int INTERCEPTOR_MAX_PERMIT_COUNT = 8 * Runtime.getRuntime().availableProcessors();
+ private static final Semaphore interceptorSemaphore = new Semaphore(INTERCEPTOR_MAX_PERMIT_COUNT);
+ private static final Map interceptorByNameMap = new LinkedHashMap<>();
+
+
+ /**
+ * Stores maps for each page class. Each map contains single patterns as keys
+ * and Link instances as values.
+ */
+ private static final ConcurrentMap, Map> linksByPage = new ConcurrentHashMap<>();
+
+ /**
+ * Stores page classes by their names.
+ */
+ private static final ConcurrentMap> classesByName = new ConcurrentHashMap<>();
+
+ /**
+ * Stores link sections by links.
+ */
+ private static final ConcurrentMap> sectionsByLinkText = new ConcurrentHashMap<>();
+
+ private static List getLinksViaReflection(Class extends Page> clazz) {
+ List result = new ArrayList<>();
+ Link link = clazz.getAnnotation(Link.class);
+ if (link != null) {
+ result.add(link);
+ }
+ LinkSet linkSet = clazz.getAnnotation(LinkSet.class);
+ if (linkSet != null) {
+ result.addAll(Arrays.asList(linkSet.value()));
+ }
+ return result;
+ }
+
+ /**
+ * Use the method to get page link name. Do not use page.getClass().getSimpleName() because of two reasons:
+ * - page can have @Name annotation,
+ * - page can be actually inherited from expected ConcretePage class because of IoC.
+ *
+ * @param page Page instance.
+ * @return Page link name.
+ */
+ public static String getLinkName(@Nonnull Page page) {
+ return getLinkName(page.getClass());
+ }
+
+ /**
+ * Use the method to get page class link name. Do not use pageClass.getSimpleName() because of two reasons:
+ * - page class can have @Name annotation,
+ * - page class can be actually inherited from expected ConcretePage class because of IoC.
+ *
+ * @param pageClass Page class.
+ * @return Page link name.
+ */
+ public static String getLinkName(@Nonnull Class extends Page> pageClass) {
+ Class> clazz = pageClass;
+
+ while (clazz != null && clazz.getAnnotation(Link.class) == null && clazz.getAnnotation(LinkSet.class) == null) {
+ clazz = clazz.getSuperclass();
+ }
+
+ if (clazz == null) {
+ logger.error("Page class should have @Link or @LinkSet annotation, but "
+ + pageClass.getName() + " hasn't.");
+ throw new NocturneException("Page class should have @Link or @LinkSet annotation, but "
+ + pageClass.getName() + " hasn't.");
+ }
+
+ Name name = clazz.getAnnotation(Name.class);
+ if (name == null) {
+ return clazz.getSimpleName();
+ } else {
+ return name.value();
+ }
+ }
+
+ /**
+ * @param clazz Page class to be added into Links.
+ * After it you can get it's link via getLink, or using @link directive
+ * from template. Link may contain template sections, like "profile/{handle}".
+ * @param linkSet List of links to be added for class {@code clazz}.
+ */
+ public static void add(Class extends Page> clazz, List linkSet) {
+ addLinkLock.lock();
+
+ try {
+ String name = getLinkName(clazz);
+ if (classesByName.containsKey(name) && !clazz.equals(classesByName.get(name))) {
+ logger.error("Can't add page which is not unique by it's name: " + clazz.getName() + '.');
+ throw new ConfigurationException("Can't add page which is not unique by it's name: "
+ + clazz.getName() + '.');
+ }
+ classesByName.put(name, clazz);
+
+ Map links = getLinksByPageClass(clazz);
+ if (links == null) {
+ // It is important that used synchronizedMap, because of "synchronized(links) {..}" later in code.
+ links = Collections.synchronizedMap(new LinkedHashMap());
+ }
+
+ for (Link link : linkSet) {
+ String[] pageLinks = StringUtil.Patterns.SEMICOLON_PATTERN.split(link.value());
+ for (String pageLink : pageLinks) {
+ if (!sectionsByLinkText.containsKey(pageLink)) {
+ sectionsByLinkText.putIfAbsent(pageLink, parseLinkToLinkSections(pageLink));
+ }
+
+ for (Map linkMap : linksByPage.values()) {
+ if (linkMap.containsKey(pageLink)) {
+ logger.error("Page link \"" + pageLink + "\" already registered.");
+ throw new ConfigurationException("Page link \"" + pageLink + "\" already registered.");
+ }
+ }
+ if (links.containsKey(pageLink)) {
+ logger.error("Page link \"" + pageLink + "\" already registered.");
+ throw new ConfigurationException("Page link \"" + pageLink + "\" already registered.");
+ }
+
+ links.put(pageLink, link);
+ }
+ }
+
+ linksByPage.put(clazz, links);
+ } finally {
+ addLinkLock.unlock();
+ }
+ }
+
+ /**
+ * @param clazz Page class to be added into Links.
+ * After it you can get it's link via getLink, or using @link directive
+ * from template. Link may contain template sections, like "profile/{handle}".
+ */
+ public static void add(Class extends Page> clazz) {
+ List linkSet = getLinksViaReflection(clazz);
+ if (linkSet.isEmpty()) {
+ logger.error("Can't find link for page " + clazz.getName() + '.');
+ throw new ConfigurationException("Can't find link for page " + clazz.getName() + '.');
+ }
+
+ add(clazz, linkSet);
+ }
+
+ /**
+ * @param clazz Page class.
+ * @param linkName desired {@link Link#name() name} of the link
+ * @param params parameters for substitution (for example link "profile/{handle}"
+ * may use "handle" key in the map.
+ * @return link for page. If there many links for page, returns one of them, which matches better
+ * @throws NoSuchLinkException if no such link exists
+ */
+ @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod"})
+ public static String getLinkByMap(Class extends Page> clazz, @Nullable String linkName, Map params) {
+ MutableBoolean multiValueParams = new MutableBoolean();
+ Map> nonEmptyParams = getNonEmptyParams(params, multiValueParams);
+
+ int bestMatchedCount = -1;
+ List bestMatchedLinkSections = null;
+ Link bestMatchedLink = null;
+
+ for (Map.Entry entry : getLinksByPageClass(clazz).entrySet()) {
+ if (linkName != null && !linkName.isEmpty() && !linkName.equals(entry.getValue().name())) {
+ continue;
+ }
+
+ List sections = sectionsByLinkText.get(entry.getKey());
+ boolean matched = true;
+ int matchedCount = 0;
+ for (LinkSection section : sections) {
+ if (section.isParameter()) {
+ ++matchedCount;
+ List values = nonEmptyParams.get(section.getParameterName());
+ String value = values == null ? null : values.get(0);
+ if (value == null || !section.isSuitable(value)) {
+ matched = false;
+ break;
+ }
+ }
+ }
+ if (matched && matchedCount > bestMatchedCount) {
+ bestMatchedCount = matchedCount;
+ bestMatchedLinkSections = sections;
+ bestMatchedLink = entry.getValue();
+ }
+ }
+
+ if (bestMatchedLinkSections == null) {
+ if (linkName == null || linkName.isEmpty()) {
+ throw new NoSuchLinkException("Can't find link for page " + clazz.getName() + '.');
+ } else {
+ throw new NoSuchLinkException("Can't find link with name '"
+ + linkName + "' for page " + clazz.getName() + '.');
+ }
+ }
+
+ StringBuilder result = new StringBuilder(ApplicationContext.getInstance().getContextPath());
+ Set usedKeys = new HashSet<>();
+
+ for (LinkSection section : bestMatchedLinkSections) {
+ String item;
+
+ if (section.isParameter()) {
+ usedKeys.add(section.getParameterName());
+ item = nonEmptyParams.get(section.getParameterName()).get(0);
+ } else {
+ item = section.getValue();
+ }
+
+ result.append('/').append(item);
+ }
+
+ if (nonEmptyParams.size() > usedKeys.size() || multiValueParams.isTrue()) {
+ boolean first = true;
+ for (Map.Entry> entry : nonEmptyParams.entrySet()) {
+ List values = entry.getValue();
+ int valueCount = values.size();
+ int startIndex = valueCount;
+
+ if (usedKeys.contains(entry.getKey())) {
+ if (valueCount > 1) {
+ startIndex = 1;
+ }
+ } else {
+ startIndex = 0;
+ }
+
+ for (int i = startIndex; i < valueCount; ++i) {
+ if (first) {
+ result.append('?');
+ first = false;
+ } else {
+ result.append('&');
+ }
+
+ try {
+ result.append(entry.getKey()).append('=').append(URLEncoder.encode(values.get(i), StandardCharsets.UTF_8.name()));
+ } catch (UnsupportedEncodingException e) {
+ // No operations.
+ }
+ }
+ }
+ }
+
+ String linkResult = result.toString();
+
+ interceptorSemaphore.acquireUninterruptibly();
+ try {
+ for (Map.Entry e : interceptorByNameMap.entrySet()) {
+ boolean skip = false;
+ for (String skipInterceptor : bestMatchedLink.skipInterceptors()) {
+ if (skipInterceptor.equals(e.getKey())) {
+ skip = true;
+ break;
+ }
+ }
+ if (!skip) {
+ linkResult = e.getValue().postprocess(linkResult, clazz, linkName, params);
+ }
+ }
+ } finally {
+ interceptorSemaphore.release();
+ }
+
+ return linkResult;
+ }
+
+ private static Map> getNonEmptyParams(Map params, MutableBoolean multiValueParams) {
+ multiValueParams.setValue(false);
+ Map> nonEmptyParams = new LinkedHashMap<>();
+
+ for (Map.Entry entry : params.entrySet()) {
+ Object value = entry.getValue();
+ if (!isMissingValue(value)) {
+ List list = toStringList(value);
+ int count = list.size();
+
+ if (count > 0) {
+ nonEmptyParams.put(entry.getKey(), list);
+
+ if (count > 1) {
+ multiValueParams.setValue(true);
+ }
+ }
+ }
+ }
+
+ return nonEmptyParams;
+ }
+
+ @Nonnull
+ private static List toStringList(@Nonnull Object value) {
+ if (value instanceof TemplateSequenceModel) {
+ return toStringList((TemplateSequenceModel) value);
+ } else if (value instanceof Collection) {
+ return toStringList((Collection) value);
+ } else if (value.getClass().isArray()) {
+ int count = Array.getLength(value);
+ List list = new ArrayList<>(count);
+
+ for (int i = 0; i < count; ++i) {
+ Object item = Array.get(value, i);
+ if (item != null) {
+ list.add(item.toString());
+ }
+ }
+
+ return list;
+ } else {
+ return new SingleEntryList<>(value.toString());
+ }
+ }
+
+ private static List toStringList(@Nonnull TemplateSequenceModel sequence) {
+ int count = getSize(sequence);
+ List list = new ArrayList<>(count);
+
+ for (int i = 0; i < count; ++i) {
+ TemplateModel item;
+ try {
+ item = sequence.get(i);
+ } catch (TemplateModelException e) {
+ logger.error("Can't get item of Freemarker sequence.", e);
+ throw new NocturneException("Can't get item of Freemarker sequence.", e);
+ }
+
+ if (item != null) {
+ list.add(item.toString());
+ }
+ }
+
+ return list;
+ }
+
+ private static List toStringList(@Nonnull Collection collection) {
+ List list = new ArrayList<>(collection.size());
+
+ for (Object item : collection) {
+ if (item != null) {
+ list.add(item.toString());
+ }
+ }
+
+ return list;
+ }
+
+ private static int getSize(@Nonnull TemplateSequenceModel sequence) {
+ try {
+ return sequence.size();
+ } catch (TemplateModelException e) {
+ logger.error("Can't get size of Freemarker sequence.", e);
+ throw new NocturneException("Can't get size of Freemarker sequence.", e);
+ }
+ }
+
+ /**
+ * @param clazz Page class.
+ * @param params parameters for substitution (for example link "profile/{handle}"
+ * may use "handle" key in the map.
+ * @return Returns link for page. If there many links for page, returns
+ * one of them, which matches better. Throws NoSuchLinkException
+ * if no such link exists.
+ */
+ public static String getLinkByMap(Class extends Page> clazz, Map params) {
+ return getLinkByMap(clazz, null, params);
+ }
+
+ /**
+ * @param name Page name.
+ * @param linkName desired {@link Link#name() name} of the link
+ * @param params parameters for substitution (for example link "profile/{handle}"
+ * may use "handle" key in the map.
+ * @return Returns link for page. If there many links for page, returns
+ * one of them, which matches better. Throws NoSuchLinkException
+ * if no such link exists.
+ */
+ public static String getLinkByMap(String name, @Nullable String linkName, Map params) {
+ Class extends Page> clazz = classesByName.get(name);
+
+ if (clazz == null) {
+ logger.error("Can't find link for page " + name + '.');
+ throw new NoSuchLinkException("Can't find link for page " + name + '.');
+ } else {
+ return getLinkByMap(clazz, linkName, params);
+ }
+ }
+
+ /**
+ * @param name Page name.
+ * @param params parameters for substitution (for example link "profile/{handle}"
+ * may use "handle" key in the map.
+ * @return Returns link for page. If there many links for page, returns
+ * one of them, which matches better. Throws NoSuchLinkException
+ * if no such link exists.
+ */
+ public static String getLinkByMap(String name, Map params) {
+ return getLinkByMap(name, null, params);
+ }
+
+ private static boolean isMissingValue(Object value) {
+ if (value == null) {
+ return true;
+ }
+
+ if (value instanceof TemplateSequenceModel) {
+ return getSize((TemplateSequenceModel) value) <= 0;
+ } else if (value instanceof Collection) {
+ return ((Collection) value).isEmpty();
+ } else if (value.getClass().isArray()) {
+ return Array.getLength(value) <= 0;
+ } else {
+ return value.toString().isEmpty();
+ }
+ }
+
+ /**
+ * @param params Array of values.
+ * @return Correspondent map.
+ */
+ private static Map convertArrayToMap(Object... params) {
+ int paramCount = params.length;
+
+ if (paramCount == 0) {
+ return Collections.emptyMap();
+ }
+
+ if (paramCount % 2 != 0) {
+ logger.error("Params should contain even number of elements.");
+ throw new IllegalArgumentException("Params should contain even number of elements.");
+ }
+
+ Map map = new LinkedHashMap<>();
+
+ for (int paramIndex = 0; paramIndex < paramCount; paramIndex += 2) {
+ map.put(params[paramIndex].toString(), params[paramIndex + 1]);
+ }
+
+ return map;
+ }
+
+ /**
+ * @param pageClass Page class.
+ * @return link for page. If there many links for page, returns one of them, which matches better
+ * @throws NoSuchLinkException if no such link exists
+ */
+ public static String getLink(Class extends Page> pageClass) {
+ return getLinkByMap(pageClass, null, Collections.emptyMap());
+ }
+
+ /**
+ * @param pageClass Page class.
+ * @param params Even length sequence of Objects. Even elements mean keys and odd
+ * values of parameters map. For example ["handle", "MikeMirzayanov", "topic", 123]
+ * means map {@literal ["handle" => "MikeMirzayanov", "topic" => 123]}. Method skips params with null value.
+ * @return link for page. If there many links for page, returns one of them, which matches better
+ * @throws NoSuchLinkException if no such link exists
+ */
+ public static String getLink(Class extends Page> pageClass, Object... params) {
+ return getLinkByMap(pageClass, null, convertArrayToMap(params));
+ }
+
+ /**
+ * @param name Page name.
+ * @param params Even length sequence of Objects. Even elements mean keys and odd
+ * values of parameters map. For example ["handle", "MikeMirzayanov", "topic", 123]
+ * means map {@literal ["handle" => "MikeMirzayanov", "topic" => 123]}. Method skips params with null value.
+ * @return link for page. If there many links for page, returns one of them, which matches better
+ * @throws NoSuchLinkException if no such link exists
+ */
+ public static String getLink(String name, Object... params) {
+ return getLinkByMap(name, null, convertArrayToMap(params));
+ }
+
+ /**
+ * @param link Relative link to the page started from "/".
+ * For example, "/profile/MikeMirzayanov".
+ * @return Result instance or {@code null} if not found.
+ */
+ public static LinkMatchResult match(String link) {
+ // Remove anchor.
+ if (link.contains("#")) {
+ link = link.substring(0, link.lastIndexOf('#'));
+ }
+
+ // Remove query string.
+ if (link.contains("?")) {
+ link = link.substring(0, link.lastIndexOf('?'));
+ }
+
+ if (!link.startsWith("/")) {
+ logger.error("Link \"" + link + "\" doesn't start with '/'.");
+ throw new IllegalArgumentException("Link \"" + link + "\" doesn't start with '/'.");
+ }
+
+ String[] linkTokens = StringUtil.Patterns.SLASH_PATTERN.split(link.substring(1));
+
+ for (Map.Entry, Map> listEntry : linksByPage.entrySet()) {
+ Map patterns = listEntry.getValue();
+ if (patterns == null) {
+ continue;
+ }
+
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (patterns) {
+ for (Map.Entry patternEntry : patterns.entrySet()) {
+ String linkText = patternEntry.getKey();
+ Map attrs = match(linkTokens, linkText);
+
+ if (attrs != null) {
+ return new LinkMatchResult(listEntry.getKey(), linkText, attrs, patternEntry.getValue());
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param linkTokens For example, ["profile", "MikeMirzayanov"] for requested link "/profile/MikeMirzayanov".
+ * @param linkText Link pattern, like "profile/{handle}".
+ * @return Correspondent params map or {@code null} if not matched.
+ */
+ private static Map match(String[] linkTokens, String linkText) {
+ List sections = sectionsByLinkText.get(linkText);
+ if (sections == null) {
+ logger.error("Can't find sections for linkText=\"" + linkText + "\".");
+ throw new NocturneException("Can't find sections for linkText=\"" + linkText + "\".");
+ }
+
+ int linkTokenCount = linkTokens.length;
+
+ if (linkTokenCount == sections.size()) {
+ Map attrs = new HashMap<>();
+
+ for (int linkTokenIndex = 0; linkTokenIndex < linkTokenCount; ++linkTokenIndex) {
+ LinkSection section = sections.get(linkTokenIndex);
+
+ if (section.isParameter()) {
+ if (!section.isSuitable(linkTokens[linkTokenIndex])) {
+ return null;
+ }
+ attrs.put(section.getParameterName(), linkTokens[linkTokenIndex]);
+ } else {
+ if (!section.getValue().equals(linkTokens[linkTokenIndex])) {
+ return null;
+ }
+ }
+ }
+
+ return attrs;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * If case of getLink-like methods if they can't find requested link.
+ */
+ @SuppressWarnings({"DeserializableClassInSecureContext", "UncheckedExceptionClass"})
+ public static class NoSuchLinkException extends RuntimeException {
+ /**
+ * @param message Error message.
+ */
+ public NoSuchLinkException(String message) {
+ super(message);
+ }
+ }
+
+ private static List parseLinkToLinkSections(String linkText) {
+ if (linkText == null || linkText.startsWith("/") || linkText.endsWith("/")) {
+ logger.error("Page link has illegal format, use links like 'home', 'page/{index}', " +
+ "'page/{index(long,positive):1,2,3}', 'section/{name(string,!blank):!a,b,c}'.");
+ throw new ConfigurationException("Page link has illegal format, use links like 'home', 'page/{index}', " +
+ "'page/{index(long,positive):1,2,3}', 'section/{name(string,!blank):!a,b,c}'."
+ );
+ }
+
+ String[] sections = StringUtil.Patterns.SLASH_PATTERN.split(linkText);
+ List linkSections = new ArrayList<>(sections.length);
+ for (String section : sections) {
+ linkSections.add(new LinkSection(section));
+ }
+
+ return linkSections;
+ }
+
+ private static final class LinkSection {
+ private final String section;
+ private final boolean parameter;
+ private final String value;
+ private final String parameterName;
+ private final List parameterRestrictions;
+ private final Set allowedParameterValues;
+ private final Set forbiddenParameterValues;
+
+ /**
+ * @param section Each part of link, i.e. "home" from link "test/home"
+ */
+ @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod", "OverlyNestedMethod"})
+ private LinkSection(String section) {
+ section = StringUtil.trim(section);
+ this.section = section;
+
+ if (section.startsWith("{") && section.endsWith("}")) {
+ parameter = true;
+ value = null;
+
+ String[] parts = StringUtil.Patterns.COLON_PATTERN.split(section.substring(1, section.length() - 1));
+ int partCount = parts.length;
+
+ if (partCount >= 1 && partCount <= 2) {
+ String namePart = StringUtil.trimToNull(parts[0]);
+ if (namePart == null) {
+ throw getInvalidSectionException(section);
+ }
+ int namePartLength = namePart.length();
+
+ parameterRestrictions = new ArrayList<>(0);
+
+ if (namePart.charAt(namePartLength - 1) == ')') {
+ int openParenthesisIndex = namePart.indexOf('(');
+
+ if (openParenthesisIndex >= 0) {
+ parameterName = namePart.substring(0, openParenthesisIndex);
+
+ String[] restrictionRules = StringUtil.Patterns.COMMA_PATTERN.split(
+ namePart.substring(openParenthesisIndex + 1, namePartLength - 1)
+ );
+
+ for (String restrictionRule : restrictionRules) {
+ if (StringUtil.isEmpty(restrictionRule = StringUtil.trim(restrictionRule))) {
+ continue;
+ }
+
+ parameterRestrictions.add(getParameterRestriction(section, restrictionRule));
+ }
+ } else {
+ parameterName = namePart;
+ }
+ } else {
+ parameterName = namePart;
+ }
+
+ if (partCount == 1) {
+ allowedParameterValues = null;
+ forbiddenParameterValues = null;
+ } else {
+ allowedParameterValues = new HashSet<>();
+ forbiddenParameterValues = new HashSet<>();
+
+ for (String valueRule : StringUtil.Patterns.COMMA_PATTERN.split(parts[1])) {
+ if (StringUtil.isEmpty(valueRule = StringUtil.trim(valueRule))) {
+ continue;
+ }
+
+ if (valueRule.charAt(0) == '!') {
+ forbiddenParameterValues.add(valueRule.substring(1));
+ } else {
+ allowedParameterValues.add(valueRule);
+ }
+ }
+
+ if (!allowedParameterValues.isEmpty()) {
+ parameterRestrictions.add(new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return allowedParameterValues.contains(value);
+ }
+ });
+ }
+
+ if (!forbiddenParameterValues.isEmpty()) {
+ parameterRestrictions.add(new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return !forbiddenParameterValues.contains(value);
+ }
+ });
+ }
+ }
+ } else {
+ throw getInvalidSectionException(section);
+ }
+ } else {
+ parameter = false;
+ value = section;
+ parameterName = null;
+ parameterRestrictions = null;
+ allowedParameterValues = null;
+ forbiddenParameterValues = null;
+ }
+ }
+
+ public boolean isParameter() {
+ return parameter;
+ }
+
+ public String getValue() {
+ ensureValueSection("value");
+ return value;
+ }
+
+ public String getParameterName() {
+ ensureParameterSection("parameterName");
+ return parameterName;
+ }
+
+ public List getParameterRestrictions() {
+ ensureParameterSection("parameterRestrictions");
+ return Collections.unmodifiableList(parameterRestrictions);
+ }
+
+ public boolean isSuitable(String value) {
+ for (ParameterRestriction parameterRestriction : getParameterRestrictions()) {
+ if (!parameterRestriction.isSuitable(value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private void ensureValueSection(String fieldName) {
+ if (parameter) {
+ logger.error("Can't read field '" + fieldName + "' of non-value section '" + section + "'.");
+ throw new IllegalStateException(String.format(
+ "Can't read field '%s' of non-value section '%s'.", fieldName, section
+ ));
+ }
+ }
+
+ private void ensureParameterSection(String fieldName) {
+ if (!parameter) {
+ logger.error("Can't read field '" + fieldName + "' of non-parameter section '" + section + "'.");
+ throw new IllegalStateException(String.format(
+ "Can't read field '%s' of non-parameter section '%s'.", fieldName, section
+ ));
+ }
+ }
+
+ private static ConfigurationException getInvalidSectionException(String section) {
+ return new ConfigurationException("Link section '" + section + "' has invalid format, " +
+ "examples of valid formats: 'test', '{userName}', '{id(int):1,2,3}', " +
+ "{title(string,!empty):!title}."
+ );
+ }
+
+ private interface ParameterRestriction {
+ boolean isSuitable(String value);
+ }
+
+ private static final class NegatedParameterRestriction implements ParameterRestriction {
+ @Nonnull
+ private final ParameterRestriction parameterRestriction;
+
+ private NegatedParameterRestriction(@Nonnull ParameterRestriction parameterRestriction) {
+ this.parameterRestriction = parameterRestriction;
+ }
+
+ @Override
+ public boolean isSuitable(String value) {
+ return !parameterRestriction.isSuitable(value);
+ }
+ }
+
+ private static ParameterRestriction getParameterRestriction(String section, String restrictionRule) {
+ if (restrictionRule != null && restrictionRule.startsWith("!")) {
+ return new NegatedParameterRestriction(internalGetParameterRestriction(
+ section, restrictionRule.substring(1)
+ ));
+ } else {
+ return internalGetParameterRestriction(section, restrictionRule);
+ }
+ }
+
+ @SuppressWarnings({"OverlyComplexMethod", "OverlyLongMethod"})
+ @Nonnull
+ private static ParameterRestriction internalGetParameterRestriction(String section, String restrictionRule) {
+ if ("null".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return value == null;
+ }
+ };
+ } else if ("empty".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return StringUtil.isEmpty(value);
+ }
+ };
+ } else if ("blank".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return StringUtil.isBlank(value);
+ }
+ };
+ } else if ("alpha".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return StringUtils.isAlpha(value);
+ }
+ };
+ } else if ("numeric".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return StringUtils.isNumeric(value);
+ }
+ };
+ } else if ("alphanumeric".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ return StringUtils.isAlphanumeric(value);
+ }
+ };
+ } else if ("byte".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Byte.parseByte(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("short".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Short.parseShort(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("int".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("long".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Long.parseLong(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("float".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Float.parseFloat(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("double".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ Double.parseDouble(value);
+ return true;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("positive".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) > 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("nonpositive".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) <= 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("negative".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) < 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("nonnegative".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) >= 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("zero".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) == 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else if ("nonzero".equalsIgnoreCase(restrictionRule)) {
+ return new ParameterRestriction() {
+ @Override
+ public boolean isSuitable(String value) {
+ try {
+ return Double.parseDouble(value) != 0.0D;
+ } catch (RuntimeException ignored) {
+ return false;
+ }
+ }
+ };
+ } else {
+ logger.error("Link section '" + section + "' contains unsupported parameter restriction '"
+ + restrictionRule + "'.");
+ throw new ConfigurationException(String.format(
+ "Link section '%s' contains unsupported parameter restriction '%s'.",
+ section, restrictionRule
+ ));
+ }
+ }
+ }
+
+ /**
+ * Adds interceptor to the Links. Link will be processed by interceptors before return.
+ *
+ * @param name name of the interceptor to add
+ * @param interceptor interceptor to add
+ */
+ public static void addInterceptor(String name, Interceptor interceptor) {
+ ensureInterceptorName(name);
+
+ if (interceptor == null) {
+ logger.error("Argument 'interceptor' is 'null'.");
+ throw new IllegalArgumentException("Argument 'interceptor' is 'null'.");
+ }
+
+ interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
+ try {
+ if (interceptorByNameMap.containsKey(name)) {
+ logger.error("Interceptor with name '" + name + "' already added.");
+ throw new IllegalStateException("Interceptor with name '" + name + "' already added.");
+ }
+ interceptorByNameMap.put(name, interceptor);
+ } finally {
+ interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
+ }
+ }
+
+ /**
+ * Removes interceptor from the Links.
+ *
+ * @param name name of the interceptor to remove
+ */
+ public static void removeInterceptor(String name) {
+ ensureInterceptorName(name);
+
+ interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
+ try {
+ interceptorByNameMap.remove(name);
+ } finally {
+ interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
+ }
+ }
+
+ /**
+ * Checks if specified interceptor is already added to the Links.
+ *
+ * @param name name of the interceptor to check
+ * @return {@code true} iff interceptor with specified name is added to the Links
+ */
+ public static boolean hasInterceptor(String name) {
+ ensureInterceptorName(name);
+
+ interceptorSemaphore.acquireUninterruptibly(INTERCEPTOR_MAX_PERMIT_COUNT);
+ try {
+ return interceptorByNameMap.containsKey(name);
+ } finally {
+ interceptorSemaphore.release(INTERCEPTOR_MAX_PERMIT_COUNT);
+ }
+ }
+
+ private static void ensureInterceptorName(String name) {
+ if (name == null || name.isEmpty()) {
+ logger.error("Argument 'name' is 'null' or empty.");
+ throw new IllegalArgumentException("Argument 'name' is 'null' or empty.");
+ }
+ }
+
+ private static Map getLinksByPageClass(Class extends Page> clazz) {
+ Map links;
+ Class parentClass = clazz;
+ while ((links = linksByPage.get(parentClass)) == null && parentClass.getSuperclass() != null) {
+ parentClass = parentClass.getSuperclass();
+ }
+ return links;
+ }
+
+ /**
+ * Custom link processor. You can add interceptor using {@link #addInterceptor(String, Interceptor)} method.
+ */
+ public interface Interceptor {
+ /**
+ * This method will be called to postprocess link.
+ *
+ * @param link link to process
+ * @param clazz page class
+ * @param linkName {@link Link#name() name} of the link
+ * @param params parameters of the link
+ * @return processed link
+ */
+ String postprocess(String link, Class extends Page> clazz, @Nullable String linkName, Map params);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/listener/PageRequestListener.java b/code/src/main/java/org/nocturne/listener/PageRequestListener.java
index 297c072..3952b13 100644
--- a/code/src/main/java/org/nocturne/listener/PageRequestListener.java
+++ b/code/src/main/java/org/nocturne/listener/PageRequestListener.java
@@ -1,32 +1,32 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.listener;
-
-import org.nocturne.main.Page;
-
-/**
- * You can implement this interface to handle
- * requests to application pages.
- *
- * @author Mike Mirzayanov
- */
-public interface PageRequestListener {
- /**
- * Will be called before processing specified page. Just
- * after request routing.
- *
- * @param page Page to be processed.
- */
- void beforeProcessPage(Page page);
-
- /**
- * Will be called after request processed the page.
- * It doesn't matter if page fails with exception - this
- * method will be executed.
- *
- * @param page Processed page.
- * @param t {@code null} if no throwable has been thrown.
- */
- void afterProcessPage(Page page, Throwable t);
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.listener;
+
+import org.nocturne.main.Page;
+
+/**
+ * You can implement this interface to handle
+ * requests to application pages.
+ *
+ * @author Mike Mirzayanov
+ */
+public interface PageRequestListener {
+ /**
+ * Will be called before processing specified page. Just
+ * after request routing.
+ *
+ * @param page Page to be processed.
+ */
+ void beforeProcessPage(Page page);
+
+ /**
+ * Will be called after request processed the page.
+ * It doesn't matter if page fails with exception - this
+ * method will be executed.
+ *
+ * @param page Processed page.
+ * @param t {@code null} if no throwable has been thrown.
+ */
+ void afterProcessPage(Page page, Throwable t);
+}
diff --git a/code/src/main/java/org/nocturne/main/ActionMap.java b/code/src/main/java/org/nocturne/main/ActionMap.java
index 448552e..5d9837e 100644
--- a/code/src/main/java/org/nocturne/main/ActionMap.java
+++ b/code/src/main/java/org/nocturne/main/ActionMap.java
@@ -1,229 +1,229 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import net.sf.cglib.reflect.FastClass;
-import net.sf.cglib.reflect.FastMethod;
-import org.nocturne.annotation.Action;
-import org.nocturne.annotation.Invalid;
-import org.nocturne.annotation.Parameter;
-import org.nocturne.annotation.Validate;
-import org.nocturne.exception.ConfigurationException;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.util.StringUtil;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Stores information about magic methods in the component.
- *
- * @author Mike Mirzayanov
- */
-class ActionMap {
- private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(ActionMap.class);
-
- /* Default action has empty key "". */
- private final Map actions = new ConcurrentHashMap<>();
-
- /* Default validator has empty key "". */
- private final Map validators = new ConcurrentHashMap<>();
-
- /* Default invalid method has empty key "". */
- private final Map invalids = new ConcurrentHashMap<>();
-
- ActionMap(Class extends Component> pageClass) {
- FastClass clazz = FastClass.create(pageClass);
-
- List methods = new ArrayList<>();
- Class> auxClass = pageClass;
- while (auxClass != null) {
- methods.addAll(Arrays.asList(auxClass.getDeclaredMethods()));
- auxClass = auxClass.getSuperclass();
- }
-
- for (Method method : methods) {
- processMethod(clazz, method);
- }
-
- for (Method method : methods) {
- processMethodAsDefault(clazz, method);
- }
- }
-
- private void processMethodAsDefault(FastClass clazz, Method method) {
- if (!actions.containsKey("") && "action".equals(method.getName()) && method.getParameterTypes().length == 0) {
- if (method.getReturnType() != void.class) {
- logger.error("Default action method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- throw new ConfigurationException("Default action method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- }
- actions.put("", new ActionMethod(clazz.getMethod(method), method.getAnnotation(Action.class)));
- }
-
- if (!validators.containsKey("") && "validate".equals(method.getName()) && method.getParameterTypes().length == 0) {
- if (method.getReturnType() != boolean.class) {
- logger.error("Default validation method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return boolean.");
- throw new ConfigurationException("Default validation method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return boolean.");
- }
- validators.put("", clazz.getMethod(method));
- }
-
- if (!invalids.containsKey("") && "invalid".equals(method.getName()) && method.getParameterTypes().length == 0) {
- if (method.getReturnType() != void.class) {
- logger.error("Default invalid method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- throw new ConfigurationException("Default invalid method [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- }
- invalids.put("", clazz.getMethod(method));
- }
- }
-
- private static void ensureProperlyAnnotatedParameters(Method method) {
- if (method.getParameterTypes().length != method.getParameterAnnotations().length) {
- logger.error("Expected \"method.getParameterTypes().length != method.getParameterAnnotations().length\".");
- throw new NocturneException("Expected \"method.getParameterTypes().length != method.getParameterAnnotations().length\".");
- }
-
- Annotation[][] parameterAnnotations = method.getParameterAnnotations();
- for (Annotation[] annotations : parameterAnnotations) {
- boolean hasParameter = false;
- boolean hasNamedParameter = false;
- for (Annotation annotation : annotations) {
- if (annotation instanceof Parameter) {
- hasParameter = true;
- hasNamedParameter = !StringUtil.isEmpty(((Parameter) annotation).name());
- }
- }
- if (!hasParameter) {
- logger.error("Each parameter of the method " + method.getDeclaringClass().getName()
- + '#' + method.getName() + " should be annotated with @Parameter.");
- throw new ConfigurationException("Each parameter of the method " + method.getDeclaringClass().getName()
- + '#' + method.getName() + " should be annotated with @Parameter.");
- }
- if (!hasNamedParameter) {
- logger.error("Each @Parameter in the method " + method.getDeclaringClass().getName()
- + '#' + method.getName() + " should have name.");
- throw new ConfigurationException("Each @Parameter in the method " + method.getDeclaringClass().getName()
- + '#' + method.getName() + " should have name.");
- }
- }
- }
-
- private void processMethod(FastClass clazz, Method method) {
- Action action = method.getAnnotation(Action.class);
-
- if (action != null) {
- if (actions.containsKey(action.value())) {
- logger.error("There are two or more methods for " +
- clazz.getName() + " marked with @Action[" + action.value() + "].");
- throw new ConfigurationException("There are two or more methods for " +
- clazz.getName() + " marked with @Action[" + action.value() + "].");
- }
-
- ensureProperlyAnnotatedParameters(method);
-
- if (method.getReturnType() != void.class) {
- logger.error("Method with annotation @Action [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- throw new ConfigurationException("Method with annotation @Action [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- }
-
- actions.put(action.value(), new ActionMethod(clazz.getMethod(method), action));
- }
-
- Validate validate = method.getAnnotation(Validate.class);
-
- if (validate != null) {
- if (validators.containsKey(validate.value())) {
- logger.error("There are two or more methods for " +
- clazz.getName() + " marked with @Validate[" + validate.value() + "].");
- throw new ConfigurationException("There are two or more methods for " +
- clazz.getName() + " marked with @Validate[" + validate.value() + "].");
- }
-
- ensureProperlyAnnotatedParameters(method);
-
- if (method.getReturnType() != boolean.class) {
- logger.error("Method with annotation @Validate [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return boolean.");
- throw new ConfigurationException("Method with annotation @Validate [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return boolean.");
- }
-
- validators.put(validate.value(), clazz.getMethod(method));
- }
-
- Invalid invalid = method.getAnnotation(Invalid.class);
-
- if (invalid != null) {
- if (invalids.containsKey(invalid.value())) {
- logger.error("There are two or more methods for " +
- clazz.getName() + " marked with @Invalid[" + invalid.value() + "].");
- throw new ConfigurationException("There are two or more methods for " +
- clazz.getName() + " marked with @Invalid[" + invalid.value() + "].");
- }
-
- ensureProperlyAnnotatedParameters(method);
-
- if (method.getReturnType() != void.class) {
- logger.error("Method with annotation @Invalid [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- throw new ConfigurationException("Method with annotation @Invalid [name=" + method.getName() + ", " +
- "class=" + clazz.getName() + "] should return void.");
- }
-
- invalids.put(invalid.value(), clazz.getMethod(method));
- }
- }
-
- ActionMethod getActionMethod(String action) {
- if (actions.containsKey(action)) {
- return actions.get(action);
- } else {
- return actions.get("");
- }
- }
-
- FastMethod getValidateMethod(String action) {
- if (validators.containsKey(action)) {
- return validators.get(action);
- } else {
- return validators.get("");
- }
- }
-
- FastMethod getInvalidMethod(String action) {
- if (invalids.containsKey(action)) {
- return invalids.get(action);
- } else {
- return invalids.get("");
- }
- }
-
- public static final class ActionMethod {
- private final FastMethod method;
- private final Action action;
-
- private ActionMethod(FastMethod method, Action action) {
- this.method = method;
- this.action = action;
- }
-
- public FastMethod getMethod() {
- return method;
- }
-
- public Action getAction() {
- return action;
- }
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.main;
+
+import net.sf.cglib.reflect.FastClass;
+import net.sf.cglib.reflect.FastMethod;
+import org.nocturne.annotation.Action;
+import org.nocturne.annotation.Invalid;
+import org.nocturne.annotation.Parameter;
+import org.nocturne.annotation.Validate;
+import org.nocturne.exception.ConfigurationException;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.util.StringUtil;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Stores information about magic methods in the component.
+ *
+ * @author Mike Mirzayanov
+ */
+class ActionMap {
+ private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(ActionMap.class);
+
+ /* Default action has empty key "". */
+ private final Map actions = new ConcurrentHashMap<>();
+
+ /* Default validator has empty key "". */
+ private final Map validators = new ConcurrentHashMap<>();
+
+ /* Default invalid method has empty key "". */
+ private final Map invalids = new ConcurrentHashMap<>();
+
+ ActionMap(Class extends Component> pageClass) {
+ FastClass clazz = FastClass.create(pageClass);
+
+ List methods = new ArrayList<>();
+ Class> auxClass = pageClass;
+ while (auxClass != null) {
+ methods.addAll(Arrays.asList(auxClass.getDeclaredMethods()));
+ auxClass = auxClass.getSuperclass();
+ }
+
+ for (Method method : methods) {
+ processMethod(clazz, method);
+ }
+
+ for (Method method : methods) {
+ processMethodAsDefault(clazz, method);
+ }
+ }
+
+ private void processMethodAsDefault(FastClass clazz, Method method) {
+ if (!actions.containsKey("") && "action".equals(method.getName()) && method.getParameterTypes().length == 0) {
+ if (method.getReturnType() != void.class) {
+ logger.error("Default action method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ throw new ConfigurationException("Default action method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ }
+ actions.put("", new ActionMethod(clazz.getMethod(method), method.getAnnotation(Action.class)));
+ }
+
+ if (!validators.containsKey("") && "validate".equals(method.getName()) && method.getParameterTypes().length == 0) {
+ if (method.getReturnType() != boolean.class) {
+ logger.error("Default validation method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return boolean.");
+ throw new ConfigurationException("Default validation method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return boolean.");
+ }
+ validators.put("", clazz.getMethod(method));
+ }
+
+ if (!invalids.containsKey("") && "invalid".equals(method.getName()) && method.getParameterTypes().length == 0) {
+ if (method.getReturnType() != void.class) {
+ logger.error("Default invalid method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ throw new ConfigurationException("Default invalid method [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ }
+ invalids.put("", clazz.getMethod(method));
+ }
+ }
+
+ private static void ensureProperlyAnnotatedParameters(Method method) {
+ if (method.getParameterTypes().length != method.getParameterAnnotations().length) {
+ logger.error("Expected \"method.getParameterTypes().length != method.getParameterAnnotations().length\".");
+ throw new NocturneException("Expected \"method.getParameterTypes().length != method.getParameterAnnotations().length\".");
+ }
+
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ for (Annotation[] annotations : parameterAnnotations) {
+ boolean hasParameter = false;
+ boolean hasNamedParameter = false;
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof Parameter) {
+ hasParameter = true;
+ hasNamedParameter = !StringUtil.isEmpty(((Parameter) annotation).name());
+ }
+ }
+ if (!hasParameter) {
+ logger.error("Each parameter of the method " + method.getDeclaringClass().getName()
+ + '#' + method.getName() + " should be annotated with @Parameter.");
+ throw new ConfigurationException("Each parameter of the method " + method.getDeclaringClass().getName()
+ + '#' + method.getName() + " should be annotated with @Parameter.");
+ }
+ if (!hasNamedParameter) {
+ logger.error("Each @Parameter in the method " + method.getDeclaringClass().getName()
+ + '#' + method.getName() + " should have name.");
+ throw new ConfigurationException("Each @Parameter in the method " + method.getDeclaringClass().getName()
+ + '#' + method.getName() + " should have name.");
+ }
+ }
+ }
+
+ private void processMethod(FastClass clazz, Method method) {
+ Action action = method.getAnnotation(Action.class);
+
+ if (action != null) {
+ if (actions.containsKey(action.value())) {
+ logger.error("There are two or more methods for " +
+ clazz.getName() + " marked with @Action[" + action.value() + "].");
+ throw new ConfigurationException("There are two or more methods for " +
+ clazz.getName() + " marked with @Action[" + action.value() + "].");
+ }
+
+ ensureProperlyAnnotatedParameters(method);
+
+ if (method.getReturnType() != void.class) {
+ logger.error("Method with annotation @Action [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ throw new ConfigurationException("Method with annotation @Action [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ }
+
+ actions.put(action.value(), new ActionMethod(clazz.getMethod(method), action));
+ }
+
+ Validate validate = method.getAnnotation(Validate.class);
+
+ if (validate != null) {
+ if (validators.containsKey(validate.value())) {
+ logger.error("There are two or more methods for " +
+ clazz.getName() + " marked with @Validate[" + validate.value() + "].");
+ throw new ConfigurationException("There are two or more methods for " +
+ clazz.getName() + " marked with @Validate[" + validate.value() + "].");
+ }
+
+ ensureProperlyAnnotatedParameters(method);
+
+ if (method.getReturnType() != boolean.class) {
+ logger.error("Method with annotation @Validate [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return boolean.");
+ throw new ConfigurationException("Method with annotation @Validate [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return boolean.");
+ }
+
+ validators.put(validate.value(), clazz.getMethod(method));
+ }
+
+ Invalid invalid = method.getAnnotation(Invalid.class);
+
+ if (invalid != null) {
+ if (invalids.containsKey(invalid.value())) {
+ logger.error("There are two or more methods for " +
+ clazz.getName() + " marked with @Invalid[" + invalid.value() + "].");
+ throw new ConfigurationException("There are two or more methods for " +
+ clazz.getName() + " marked with @Invalid[" + invalid.value() + "].");
+ }
+
+ ensureProperlyAnnotatedParameters(method);
+
+ if (method.getReturnType() != void.class) {
+ logger.error("Method with annotation @Invalid [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ throw new ConfigurationException("Method with annotation @Invalid [name=" + method.getName() + ", " +
+ "class=" + clazz.getName() + "] should return void.");
+ }
+
+ invalids.put(invalid.value(), clazz.getMethod(method));
+ }
+ }
+
+ ActionMethod getActionMethod(String action) {
+ if (actions.containsKey(action)) {
+ return actions.get(action);
+ } else {
+ return actions.get("");
+ }
+ }
+
+ FastMethod getValidateMethod(String action) {
+ if (validators.containsKey(action)) {
+ return validators.get(action);
+ } else {
+ return validators.get("");
+ }
+ }
+
+ FastMethod getInvalidMethod(String action) {
+ if (invalids.containsKey(action)) {
+ return invalids.get(action);
+ } else {
+ return invalids.get("");
+ }
+ }
+
+ public static final class ActionMethod {
+ private final FastMethod method;
+ private final Action action;
+
+ private ActionMethod(FastMethod method, Action action) {
+ this.method = method;
+ this.action = action;
+ }
+
+ public FastMethod getMethod() {
+ return method;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+ }
+}
diff --git a/code/src/main/java/org/nocturne/main/ApplicationContext.java b/code/src/main/java/org/nocturne/main/ApplicationContext.java
index 6374a7a..8249968 100644
--- a/code/src/main/java/org/nocturne/main/ApplicationContext.java
+++ b/code/src/main/java/org/nocturne/main/ApplicationContext.java
@@ -1,1106 +1,1106 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import com.google.common.base.Preconditions;
-import com.google.common.primitives.Ints;
-import com.google.inject.Injector;
-import org.apache.commons.lang3.ArrayUtils;
-import org.jetbrains.annotations.Contract;
-import org.nocturne.caption.Captions;
-import org.nocturne.caption.CaptionsImpl;
-import org.nocturne.collection.SingleEntryList;
-import org.nocturne.exception.ConfigurationException;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.exception.ReflectionException;
-import org.nocturne.geoip.GeoIpUtil;
-import org.nocturne.link.Link;
-import org.nocturne.module.Module;
-import org.nocturne.reset.ResetStrategy;
-import org.nocturne.util.ReflectionUtil;
-import org.nocturne.util.RequestUtil;
-import org.nocturne.util.StringUtil;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.servlet.ServletContext;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import java.io.File;
-import java.nio.charset.StandardCharsets;
-import java.util.*;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.regex.Pattern;
-
-/**
- * This is global singleton object, accessible from all levels of
- * application. Use it to get current page and component.
- *
- * @author Mike Mirzayanov
- */
-@SuppressWarnings({"WeakerAccess", "unused"})
-public class ApplicationContext {
- private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(ApplicationContext.class);
-
- /**
- * The only singleton instance.
- */
- private static final ApplicationContext INSTANCE = new ApplicationContext();
-
- private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
- /**
- * Lock to perform synchronized operations.
- */
- private final Lock lock = new ReentrantLock();
-
- /**
- * Current page. Stored as ThreadLocal.
- */
- private static final ThreadLocal currentPage = new ThreadLocal<>();
-
- /**
- * Current component. Stored as ThreadLocal.
- */
- private static final ThreadLocal currentComponent = new ThreadLocal<>();
-
- /**
- * Is in debug mode?
- */
- private boolean debug;
-
- /**
- * List of directories to be scanned for recompiled classes. Possibly, it depends on your IDE.
- */
- private Set reloadingClassPaths;
-
- /**
- * Context path of the application.
- * Use {@code null} to use ApplicationContext.getInstance().getRequest().getContextPath().
- */
- private String contextPath;
-
- /**
- * List of listener class names.
- */
- private Set pageRequestListeners;
-
- /**
- * Request router class name.
- */
- private String requestRouter;
-
- /**
- * IoC module class name.
- */
- private String guiceModuleClassName;
-
- /**
- * List of packages (or classes) which will be reloaded using ReloadingClassLoader.
- */
- private Set classReloadingPackages;
-
- /**
- * List of packages (or classes) which should not be reloaded using ReloadingClassLoader,
- * even they are in classReloadingPackages.
- */
- private Set classReloadingExceptions;
-
- /**
- * Where to find templates. Contains relative paths from
- * deployed application root. For example: WEB-INF/templates.
- */
- private String[] templatePaths;
-
- /**
- * Indicates if template loader should stick to last successful template path
- * or always check template paths in the configured order.
- * Default value is {@code true}.
- */
- private boolean stickyTemplatePaths = true;
-
- /**
- * Preprocess FTL templates to read as component templates (if ... has found).
- */
- private boolean useComponentTemplates;
-
- /**
- * Autoimported file for all component LESS styles.
- */
- private File componentTemplatesLessCommonsFile;
-
- /**
- * Servlet context.
- */
- private ServletContext servletContext;
-
- /**
- * What page to show if RequestRouter returns {@code null}.
- */
- private String defaultPageClassName;
-
- /**
- * Pattern: if request.getServletPath() (example: /some/path) matches it, request
- * ignored by nocturne.
- */
- private Pattern skipRegex;
-
- /**
- * Default locale or English by default.
- */
- private Locale defaultLocale = new Locale("en");
-
- /**
- * Where to find caption property files, used in case of CaptionsImpl used.
- */
- private String debugCaptionsDir;
-
- /**
- * Where to find resources by DebugResourceFilter.
- */
- private String debugWebResourcesDir;
-
- /**
- * Class name for Captions implementations.
- */
- private String captionsImplClass = CaptionsImpl.class.getName();
-
- /**
- * Captions implementation instance.
- */
- private Captions captions;
-
- /**
- * Encoding for caption property files, used in case of CaptionsImpl used.
- */
- private String captionFilesEncoding = StandardCharsets.UTF_8.name();
-
- /**
- * Allowed languages (use 2-letter codes). Only English by default.
- */
- private List allowedLanguages = Collections.singletonList("en");
-
- /**
- * To use geoip to setup language by 2-letter uppercase country code (ISO 3166 code).
- * The property should have a form like: RU,BY:ru;EN,GB,US,CA:en.
- */
- private Map countryToLanguage = new HashMap<>();
-
- /**
- * Default reset strategy for fields of Components: should they be reset after request processing.
- */
- private ResetStrategy resetStrategy;
-
- /**
- * Delay between checks of template files to be changed (in seconds).
- */
- private int templatesUpdateDelay = 60;
-
- /**
- * List of annotation classes to override default strategy, should be used on classes or fields.
- */
- private Set resetAnnotations;
-
- /**
- * List of annotation classes to override default strategy, should be used on classes or fields.
- */
- private Set persistAnnotations;
-
- /**
- * Guice injector.
- */
- private Injector injector;
-
- /**
- * RequestContext for current thread.
- */
- private static final ThreadLocal requestsPerThread = new ThreadLocal<>();
-
- /**
- * Reloading class loader for current thread, used in debug mode only.
- */
- private static final ThreadLocal reloadingClassLoaderPerThread = new ThreadLocal<>();
-
- /**
- * Current reloading class loader.
- */
- private ClassLoader reloadingClassLoader = getClass().getClassLoader();
-
- /**
- * List of loaded modules.
- */
- private List modules = new ArrayList<>();
-
- /**
- * ApplicationContext is initialized.
- */
- private final AtomicBoolean initialized = new AtomicBoolean(false);
-
- private final Lock initializedLock = new ReentrantLock();
- private final Condition initializedCondition = initializedLock.newCondition();
-
- void setInitialized() {
- initializedLock.lock();
- try {
- if (!initialized.getAndSet(true)) {
- initializedCondition.signalAll();
- }
- } finally {
- initializedLock.unlock();
- }
- }
-
- @SuppressWarnings("WeakerAccess")
- @Contract(pure = true)
- boolean isInitialized() {
- return initialized.get();
- }
-
- void setRequestAndResponse(HttpServletRequest request, HttpServletResponse response) {
- requestsPerThread.set(new RequestContext(request, response));
- }
-
- public void unsetRequestAndResponse() {
- requestsPerThread.set(new RequestContext(null, null));
- }
-
- /**
- * In debug mode it will return reloading class loader, and it
- * will return typical web-application class loader in production mode.
- *
- * @return Reloading class loader (for debug mode) and usual web-application
- * class loader (for production mode).
- */
- public ClassLoader getReloadingClassLoader() {
- if (debug) {
- return reloadingClassLoaderPerThread.get();
- } else {
- return reloadingClassLoader;
- }
- }
-
- /**
- * @return Returns application context path.
- * You should build paths in your application by
- * concatenation getContextPath() and relative path inside
- * the application.
- */
- public String getContextPath() {
- if (contextPath == null) {
- return getRequest().getContextPath();
- } else {
- return contextPath;
- }
- }
-
- /**
- * Where to find captions properties files if
- * naive org.nocturne.caption.CaptionsImpl backed used and
- * debug mode switched on.
- *
- * @return Directory or null in the production mode.
- */
- @Nullable
- public String getDebugCaptionsDir() {
- if (debug) {
- return debugCaptionsDir;
- } else {
- return null;
- }
- }
-
- /**
- * @return Default application locale, specified by
- * nocturne.default-language. English if no one specified.
- */
- public Locale getDefaultLocale() {
- return defaultLocale;
- }
-
- /**
- * @return List of allowed languages, use property nocturne.allowed-languages.
- */
- public List getAllowedLanguages() {
- return Collections.unmodifiableList(allowedLanguages);
- }
-
- /**
- * @return Map to setup language by 2-letter uppercase country code (ISO 3166 code).
- * The property should have a form like: RU,BY:ru;EN,GB,US,CA:en.
- */
- @SuppressWarnings("WeakerAccess")
- public Map getCountryToLanguage() {
- return Collections.unmodifiableMap(countryToLanguage);
- }
-
- /**
- * @return Default reset strategy for fields of Components: should they be reset after request processing.
- */
- public ResetStrategy getResetStrategy() {
- return resetStrategy;
- }
-
- /**
- * @return Delay between checks of template files to be changed (in seconds).
- */
- public int getTemplatesUpdateDelay() {
- return templatesUpdateDelay;
- }
-
- /**
- * @return List of annotation classes to override default strategy, should be used on classes or fields.
- */
- public Set getResetAnnotations() {
- return Collections.unmodifiableSet(resetAnnotations);
- }
-
- /**
- * @return List of annotation classes to override default strategy, should be used on classes or fields.
- */
- public Set getPersistAnnotations() {
- return Collections.unmodifiableSet(persistAnnotations);
- }
-
- void setTemplatesUpdateDelay(int templatesUpdateDelay) {
- this.templatesUpdateDelay = templatesUpdateDelay;
- }
-
- void setUseComponentTemplates(boolean useComponentTemplates) {
- this.useComponentTemplates = useComponentTemplates;
- }
-
- public boolean isUseComponentTemplates() {
- return useComponentTemplates;
- }
-
- public File getComponentTemplatesLessCommonsFile() {
- return componentTemplatesLessCommonsFile;
- }
-
- void setComponentTemplatesLessCommonsFile(File componentTemplatesLessCommonsFile) {
- this.componentTemplatesLessCommonsFile = componentTemplatesLessCommonsFile;
- }
-
- void setResetStrategy(ResetStrategy resetStrategy) {
- this.resetStrategy = resetStrategy;
- }
-
- void setResetAnnotations(Collection resetAnnotations) {
- this.resetAnnotations = new LinkedHashSet<>(resetAnnotations);
- }
-
- void setPersistAnnotations(Collection persistAnnotations) {
- this.persistAnnotations = new LinkedHashSet<>(persistAnnotations);
- }
-
- /**
- * @return What page to show if RequestRouter returns {@code null}.
- * Returns {@code null} if application should return 404 on it.
- */
- public String getDefaultPageClassName() {
- return defaultPageClassName;
- }
-
- /**
- * @return Encoding for caption files if
- * naive org.nocturne.caption.CaptionsImpl backed used.
- */
- public String getCaptionFilesEncoding() {
- return captionFilesEncoding;
- }
-
- /**
- * @return Current rendering frame or page.
- */
- public Component getCurrentComponent() {
- return currentComponent.get();
- }
-
- /**
- * @return Current rendering page instance.
- */
- public Page getCurrentPage() {
- return currentPage.get();
- }
-
- /**
- * Method to get application context.
- *
- * @return The only application context instance.
- */
- public static ApplicationContext getInstance() {
- return INSTANCE;
- }
-
- void setContextPath(String contextPath) {
- this.contextPath = contextPath;
- }
-
- void setLink(Link link) {
- getRequest().setAttribute("nocturne.current-page-link", link);
- }
-
- /**
- * @return Link annotation instance, which was choosen by LinkedRequestRouter as
- * link for current request.
- */
- public Link getLink() {
- return (Link) getRequest().getAttribute("nocturne.current-page-link");
- }
-
- void setDefaultPageClassName(String defaultPageClassName) {
- this.defaultPageClassName = defaultPageClassName;
- }
-
- void setCurrentPage(Page page) {
- currentPage.set(page);
- }
-
- /**
- * @return Is application in the debug mode?
- */
- public boolean isDebug() {
- return debug;
- }
-
- /**
- * @return Captions implementation class name.
- */
- public String getCaptionsImplClass() {
- return captionsImplClass;
- }
-
- void setCurrentComponent(Component component) {
- currentComponent.set(component);
- }
-
- /**
- * @return List of directories to be scanned for recompiled classes.
- * Used in the debug mode only.
- * Possibly, it depends on your IDE.
- * Setup it by nocturne.reloading-class-paths.
- */
- public List getReloadingClassPaths() {
- return new LinkedList<>(reloadingClassPaths);
- }
-
- /**
- * @return List of listener class names. Setup it by nocturne.page-request-listeners.
- */
- public List getPageRequestListeners() {
- return new LinkedList<>(pageRequestListeners);
- }
-
- void addRequestOverrideParameter(String name, String value) {
- requestsPerThread.get().addOverrideParameter(name, value);
- }
-
- void addRequestOverrideParameter(String name, List values) {
- requestsPerThread.get().addOverrideParameter(name, values);
- }
-
- Map> getRequestOverrideParameters() {
- return requestsPerThread.get().getOverrideParameters();
- }
-
- void setDebug(boolean debug) {
- this.debug = debug;
- }
-
- void setReloadingClassPaths(List reloadingClassPaths) {
- this.reloadingClassPaths = new LinkedHashSet<>(reloadingClassPaths);
- }
-
- void setPageRequestListeners(List pageRequestListeners) {
- this.pageRequestListeners = new LinkedHashSet<>(pageRequestListeners);
- }
-
- void setCaptionFilesEncoding(String captionFilesEncoding) {
- this.captionFilesEncoding = captionFilesEncoding;
- }
-
- void setRequestRouter(String requestRouter) {
- this.requestRouter = requestRouter;
- }
-
- void setTemplatePaths(String[] templatePaths) {
- this.templatePaths = Arrays.copyOf(templatePaths, templatePaths.length);
- }
-
- void setDefaultLocale(String defaultLanguage) {
- this.defaultLocale = new Locale(defaultLanguage.toLowerCase());
- }
-
- void setGuiceModuleClassName(String guiceModuleClassName) {
- this.guiceModuleClassName = guiceModuleClassName;
- }
-
- void setClassReloadingExceptions(List classReloadingExceptions) {
- this.classReloadingExceptions = new LinkedHashSet<>(classReloadingExceptions);
- }
-
- /**
- * @return Returns request router instance. Specify nocturne.request-router
- * property to set its class name.
- */
- public String getRequestRouter() {
- return requestRouter;
- }
-
- /**
- * @return Guice IoC module class name. Set nocturne.guice-module-class-name property.
- */
- public String getGuiceModuleClassName() {
- return guiceModuleClassName;
- }
-
- void setClassReloadingPackages(List classReloadingPackages) {
- this.classReloadingPackages = new LinkedHashSet<>(classReloadingPackages);
- }
-
- void setInjector(Injector injector) {
- lock.lock();
- try {
- this.injector = injector;
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * @return Guice injector. It is not good idea to use it.
- */
- public Injector getInjector() {
- return injector;
- }
-
- /**
- * @return List of packages (or classes) which will be reloaded using
- * ReloadingClassLoader. Set nocturne.class-reloading-packages to specify it.
- */
- public List getClassReloadingPackages() {
- return new LinkedList<>(classReloadingPackages);
- }
-
- /**
- * @return List of packages (or classes) which should not be reloaded
- * using ReloadingClassLoader, even they are in classReloadingPackages.
- * Set nocturne.class-reloading-exceptions to specify the value.
- */
- public List getClassReloadingExceptions() {
- return new LinkedList<>(classReloadingExceptions);
- }
-
- /**
- * @return Where to find templates. Contains relative paths from deployed application root. For example: WEB-INF/templates.
- * Set nocturne.template-paths - semicolon separated list of paths.
- * Set nocturne.templates-path (deprecated) - single path.
- */
- public String[] getTemplatePaths() {
- return Arrays.copyOf(templatePaths, templatePaths.length);
- }
-
- /**
- * @return Flag that indicates if template loader should stick to last successful template path
- * or always check template paths in the configured order.
- * Default value is {@code true}.
- */
- public boolean isStickyTemplatePaths() {
- return stickyTemplatePaths;
- }
-
- public void setStickyTemplatePaths(boolean stickyTemplatePaths) {
- this.stickyTemplatePaths = stickyTemplatePaths;
- }
-
- void setAllowedLanguages(List allowedLanguages) {
- this.allowedLanguages = new ArrayList<>(allowedLanguages);
- }
-
- void setCountryToLanguage(Map countryToLanguage) {
- this.countryToLanguage = new HashMap<>(countryToLanguage);
- }
-
- void setServletContext(ServletContext servletContext) {
- this.servletContext = servletContext;
- }
-
- void setDebugCaptionsDir(String debugCaptionsDir) {
- this.debugCaptionsDir = debugCaptionsDir;
- }
-
- /**
- * @return Returns current servlet context.
- */
- public ServletContext getServletContext() {
- return servletContext;
- }
-
-
- /**
- * @return if request.getServletPath() (example: /some/path) matches it, request ignored by nocturne.
- * Use nocturne.skip-regex to set it.
- */
- public Pattern getSkipRegex() {
- return skipRegex;
- }
-
- void setCaptionsImplClass(String captionsImplClass) {
- this.captionsImplClass = captionsImplClass;
- }
-
- void setSkipRegex(Pattern skipRegex) {
- this.skipRegex = skipRegex;
- }
-
- public boolean hasRequest() {
- RequestContext requestContext = requestsPerThread.get();
- return requestContext != null && requestContext.getRequest() != null;
- }
-
- /**
- * @return Returns current servlet request instance.
- */
- public HttpServletRequest getRequest() {
- return requestsPerThread.get().getRequest();
- }
-
- public boolean hasResponse() {
- RequestContext requestContext = requestsPerThread.get();
- return requestContext != null && requestContext.getResponse() != null;
- }
-
- /**
- * @return Returns current servlet response instance.
- */
- public HttpServletResponse getResponse() {
- return requestsPerThread.get().getResponse();
- }
-
- void setReloadingClassLoader(ClassLoader loader) {
- if (debug) {
- reloadingClassLoaderPerThread.set(loader);
- } else {
- reloadingClassLoader = loader;
- }
- }
-
- /**
- * Modules use the method to update reloading class path.
- * Do not use it from your code.
- *
- * @param dir Directory to be added.
- */
- public void addReloadingClassPath(File dir) {
- if (!dir.isDirectory()) {
- logger.error("Path " + dir.getName() + " expected to be a directory.");
- throw new ConfigurationException("Path " + dir.getName() + " expected to be a directory.");
- }
- reloadingClassPaths.add(dir);
-
- if (debug) {
- ReloadingContext context = ReloadingContext.getInstance();
- try {
- ReflectionUtil.invoke(context, "addReloadingClassPath", dir);
- } catch (ReflectionException e) {
- logger.error("Can't call addReloadingClassPath for ReloadingContext.", e);
- throw new NocturneException("Can't call addReloadingClassPath for ReloadingContext.", e);
- }
- } else {
- ReloadingContext.getInstance().addReloadingClassPath(dir);
- }
- }
-
- public void addClassReloadingException(String packageOrClassName) {
- if (debug) {
- classReloadingExceptions.add(packageOrClassName);
- ReloadingContext.getInstance().addClassReloadingException(packageOrClassName);
- }
- }
-
- /**
- * @param shortcut Shortcut value.
- * @return Use the method to work with captions from your code.
- * Usually, it is not good idea, because captions are part of view layer.
- */
- @SuppressWarnings("DollarSignInName")
- public String $(String shortcut) {
- return $(shortcut, ArrayUtils.EMPTY_OBJECT_ARRAY);
- }
-
- /**
- * @param shortcut Shortcut value.
- * @param args Shortcut arguments.
- * @return Use the method to work with captions from your code.
- * Usually, it is not good idea, because captions are part of view layer.
- */
- @SuppressWarnings({"OverloadedVarargsMethod", "DollarSignInName"})
- public String $(String shortcut, Object... args) {
- shortcut = shortcut.trim();
- initializeCaptions();
- return captions.find(shortcut, args);
- }
-
- /**
- * @param locale Expected locale.
- * @param shortcut Shortcut value.
- * @param args Shortcut arguments.
- * @return Use the method to work with captions from your code.
- * Usually, it is not good idea, because captions are part of view layer.
- */
- @SuppressWarnings("OverloadedVarargsMethod")
- public String getCaption(Locale locale, String shortcut, Object... args) {
- shortcut = shortcut.trim();
- initializeCaptions();
- return captions.find(locale, shortcut, args);
- }
-
- /**
- * @param locale Expected locale.
- * @param shortcut Shortcut value.
- * @return Use the method to work with captions from your code.
- * Usually, it is not good idea, because captions are part of view layer.
- */
- public String getCaption(Locale locale, String shortcut) {
- shortcut = shortcut.trim();
- initializeCaptions();
- return captions.find(locale, shortcut);
- }
-
- @SuppressWarnings("unchecked")
- private void initializeCaptions() {
- if (captions != null) {
- return;
- }
-
- lock.lock();
- try {
- Class extends Captions> clazz = (Class extends Captions>) getClass().getClassLoader().loadClass(captionsImplClass);
- captions = injector.getInstance(clazz);
- } catch (ClassNotFoundException e) {
- logger.error("Class " + captionsImplClass + " not found.", e);
- throw new ConfigurationException("Class " + captionsImplClass + " should implement Captions.", e);
- } finally {
- lock.unlock();
- }
- }
-
- /**
- * @return Locale for current request.
- */
- public Locale getLocale() {
- return requestsPerThread.get().getLocale();
- }
-
- /**
- * @return List of loaded modules.
- */
- public List getModules() {
- return Collections.unmodifiableList(modules);
- }
-
- void setModules(List modules) {
- this.modules = new ArrayList<>(modules);
- }
-
- /**
- * @return Prefix before attributes in request which
- * will be injected as parameters in Components.
- */
- public static String getAdditionalParamsRequestAttributePrefix() {
- return "nocturne.additional-parameter.";
- }
-
- private static String getActionRequestPageClassName() {
- return "nocturne.request-page-class-name";
- }
-
- private static String getActionRequestParamName() {
- return "nocturne.request-action";
- }
-
- void setRequestAction(String action) {
- getRequest().setAttribute(getActionRequestParamName(), action);
- }
-
- /**
- * @return Action for current request (how request router decided). Empty string if
- * no one specified. Typically, gets from action parameter (example: ?action=test)
- * or link template (example: "user/{action}").
- */
- public String getRequestAction() {
- return (String) getRequest().getAttribute(getActionRequestParamName());
- }
-
- void setRequestPageClassName(String pageClassName) {
- getRequest().setAttribute(getActionRequestPageClassName(), pageClassName);
- }
-
- /**
- * @return Page class name for current request (how request router decided).
- */
- public String getRequestPageClassName() {
- return (String) getRequest().getAttribute(getActionRequestPageClassName());
- }
-
- public void setDebugWebResourcesDir(String debugWebResourcesDir) {
- this.debugWebResourcesDir = debugWebResourcesDir;
- }
-
- /**
- * @return Returns the directory where to find resources by DebugResourceFilter.
- */
- public String getDebugWebResourcesDir() {
- return debugWebResourcesDir;
- }
-
- /**
- * @param runnable Runnable to be executed after ApplicationContext has been initialized.
- */
- public void executeAfterInitialization(Runnable runnable) {
- new Thread(() -> {
- initializedLock.lock();
- try {
- while (!isInitialized()) {
- try {
- initializedCondition.await();
- } catch (InterruptedException ignored) {
- // No operations.
- }
- }
- runnable.run();
- } finally {
- initializedLock.unlock();
- }
- }).start();
- }
-
- /**
- * Stores current request context: request, response and locale.
- */
- private static final class RequestContext {
- private static final Pattern ACCEPT_LANGUAGE_SPLIT_PATTERN = Pattern.compile("[,;-]");
- private static final String LANGUAGE_COOKIE_NAME = "nocturne.language";
- /**
- * Http servlet request.
- */
- private final HttpServletRequest request;
-
- /**
- * Http servlet response.
- */
- private final HttpServletResponse response;
-
- /**
- * Locale for current request.
- */
- private Locale locale;
-
- /**
- * Parameters which override request params.
- */
- private Map> overrideParameters;
-
- private RequestContext(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) {
- if ((request == null) ^ (response == null)) {
- logger.error("It is not possible case '(request == null) ^ (response == null)'.");
- throw new IllegalArgumentException("It is not possible case '(request == null) ^ (response == null)'.");
- }
-
- this.request = request;
- this.response = response;
-
- if (request == null) {
- this.locale = null;
- } else {
- setupLocale();
- }
- }
-
- /**
- * @return Http servlet request.
- */
- public HttpServletRequest getRequest() {
- return request;
- }
-
- /**
- * @return Http servlet response.
- */
- public HttpServletResponse getResponse() {
- return response;
- }
-
- /**
- * @return Locale for current request.
- * Uses lang, language, locale parameters to find
- * current locale: 2-letter language code.
- * If it specified once, stores current locale in the session.
- */
- public Locale getLocale() {
- return locale;
- }
-
- @Contract(value = "null -> true", pure = true)
- private static boolean isInvalidLanguage(@Nullable String lang) {
- return lang == null || lang.length() != 2;
- }
-
- private void setupLocale() {
- Map> requestMap = RequestUtil.getRequestParams(request);
-
- String lang = RequestUtil.getFirst(requestMap, "lang");
-
- if (isInvalidLanguage(lang)) {
- lang = RequestUtil.getFirst(requestMap, "language");
- }
-
- if (isInvalidLanguage(lang)) {
- lang = RequestUtil.getFirst(requestMap, "locale");
- }
-
- if (isInvalidLanguage(lang)) {
- HttpSession session = request.getSession(false);
- if (session != null) {
- lang = (String) session.getAttribute("nocturne.language");
- }
-
- if (isInvalidLanguage(lang)) {
- lang = getCookie(LANGUAGE_COOKIE_NAME);
- }
-
- if (isInvalidLanguage(lang)) {
- lang = getLanguageByGeoIp();
- if (isInvalidLanguage(lang)) {
- String[] languages = getAcceptLanguages();
- for (String language : languages) {
- if (getInstance().getAllowedLanguages().contains(language)
- && !"en".equalsIgnoreCase(language)) {
- lang = language;
- break;
- }
- }
-
- if (isInvalidLanguage(lang)) {
- for (String language : languages) {
- if (getInstance().getAllowedLanguages().contains(language)) {
- lang = language;
- break;
- }
- }
- }
- }
- }
- locale = localeByLanguage(lang);
- } else {
- locale = localeByLanguage(lang);
- request.getSession().setAttribute("nocturne.language", locale.getLanguage());
- addCookie(LANGUAGE_COOKIE_NAME, lang, TimeUnit.DAYS.toSeconds(30));
- }
- }
-
- @SuppressWarnings("SameParameterValue")
- @Nullable
- private String getCookie(String cookieName) {
- Cookie[] cookies = request.getCookies();
- if (cookies != null) {
- for (Cookie cookie : cookies) {
- if (cookieName.equals(cookie.getName())) {
- return cookie.getValue();
- }
- }
- }
- return null;
- }
-
- @SuppressWarnings("SameParameterValue")
- private void addCookie(String cookieName, String cookieValue, long maxAge) {
- boolean updated = false;
- if (request.getCookies() != null) {
- for (Cookie cookie : request.getCookies()) {
- if (cookie.getName().equals(cookieName)) {
- cookie.setValue(cookieValue);
- if (updated) {
- cookie.setMaxAge(0);
- } else {
- cookie.setMaxAge(Ints.checkedCast(maxAge));
- }
- cookie.setPath("/");
- response.addCookie(cookie);
- updated = true;
- }
- }
- }
-
- if (!updated) {
- Cookie cookie = new Cookie(cookieName, cookieValue);
- cookie.setMaxAge(Ints.checkedCast(maxAge));
- cookie.setPath("/");
- response.addCookie(cookie);
- }
- }
-
- @Nullable
- private String getLanguageByGeoIp() {
- String countryCode = null; // GeoIpUtil.getCountryCode(request);
- String lang = getInstance().getCountryToLanguage().get(countryCode);
- String[] languages = getAcceptLanguages();
-
- if (ArrayUtils.indexOf(languages, lang) >= 0 && getInstance().getAllowedLanguages().contains(lang)) {
- return lang;
- }
-
- return null;
- }
-
- private String[] getAcceptLanguages() {
- String header = request.getHeader("Accept-Language");
-
- if (StringUtil.isEmpty(header)) {
- return EMPTY_STRING_ARRAY;
- } else {
- String[] result = ACCEPT_LANGUAGE_SPLIT_PATTERN.split(header);
- for (int i = 0; i < result.length; ++i) {
- result[i] = result[i].toLowerCase();
- }
- return result;
- }
- }
-
- @Nonnull
- private static Locale localeByLanguage(@Nullable String language) {
- if (getInstance().getAllowedLanguages().contains(language)) {
- return new Locale(Preconditions.checkNotNull(language));
- } else {
- return getInstance().getDefaultLocale();
- }
- }
-
- private void addOverrideParameter(String name, String value) {
- if (overrideParameters == null) {
- overrideParameters = new HashMap<>();
- }
-
- overrideParameters.put(name, new SingleEntryList<>(value));
- }
-
- private void addOverrideParameter(String name, Collection values) {
- if (overrideParameters == null) {
- overrideParameters = new HashMap<>();
- }
-
- overrideParameters.put(name, new ArrayList<>(values));
- }
-
- private Map> getOverrideParameters() {
- return overrideParameters;
- }
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.main;
+
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Ints;
+import com.google.inject.Injector;
+import org.apache.commons.lang3.ArrayUtils;
+import org.jetbrains.annotations.Contract;
+import org.nocturne.caption.Captions;
+import org.nocturne.caption.CaptionsImpl;
+import org.nocturne.collection.SingleEntryList;
+import org.nocturne.exception.ConfigurationException;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.exception.ReflectionException;
+import org.nocturne.geoip.GeoIpUtil;
+import org.nocturne.link.Link;
+import org.nocturne.module.Module;
+import org.nocturne.reset.ResetStrategy;
+import org.nocturne.util.ReflectionUtil;
+import org.nocturne.util.RequestUtil;
+import org.nocturne.util.StringUtil;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
+
+/**
+ * This is global singleton object, accessible from all levels of
+ * application. Use it to get current page and component.
+ *
+ * @author Mike Mirzayanov
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class ApplicationContext {
+ private static final org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(ApplicationContext.class);
+
+ /**
+ * The only singleton instance.
+ */
+ private static final ApplicationContext INSTANCE = new ApplicationContext();
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ /**
+ * Lock to perform synchronized operations.
+ */
+ private final Lock lock = new ReentrantLock();
+
+ /**
+ * Current page. Stored as ThreadLocal.
+ */
+ private static final ThreadLocal currentPage = new ThreadLocal<>();
+
+ /**
+ * Current component. Stored as ThreadLocal.
+ */
+ private static final ThreadLocal currentComponent = new ThreadLocal<>();
+
+ /**
+ * Is in debug mode?
+ */
+ private boolean debug;
+
+ /**
+ * List of directories to be scanned for recompiled classes. Possibly, it depends on your IDE.
+ */
+ private Set reloadingClassPaths;
+
+ /**
+ * Context path of the application.
+ * Use {@code null} to use ApplicationContext.getInstance().getRequest().getContextPath().
+ */
+ private String contextPath;
+
+ /**
+ * List of listener class names.
+ */
+ private Set pageRequestListeners;
+
+ /**
+ * Request router class name.
+ */
+ private String requestRouter;
+
+ /**
+ * IoC module class name.
+ */
+ private String guiceModuleClassName;
+
+ /**
+ * List of packages (or classes) which will be reloaded using ReloadingClassLoader.
+ */
+ private Set classReloadingPackages;
+
+ /**
+ * List of packages (or classes) which should not be reloaded using ReloadingClassLoader,
+ * even they are in classReloadingPackages.
+ */
+ private Set classReloadingExceptions;
+
+ /**
+ * Where to find templates. Contains relative paths from
+ * deployed application root. For example: WEB-INF/templates.
+ */
+ private String[] templatePaths;
+
+ /**
+ * Indicates if template loader should stick to last successful template path
+ * or always check template paths in the configured order.
+ * Default value is {@code true}.
+ */
+ private boolean stickyTemplatePaths = true;
+
+ /**
+ * Preprocess FTL templates to read as component templates (if ... has found).
+ */
+ private boolean useComponentTemplates;
+
+ /**
+ * Autoimported file for all component LESS styles.
+ */
+ private File componentTemplatesLessCommonsFile;
+
+ /**
+ * Servlet context.
+ */
+ private ServletContext servletContext;
+
+ /**
+ * What page to show if RequestRouter returns {@code null}.
+ */
+ private String defaultPageClassName;
+
+ /**
+ * Pattern: if request.getServletPath() (example: /some/path) matches it, request
+ * ignored by nocturne.
+ */
+ private Pattern skipRegex;
+
+ /**
+ * Default locale or English by default.
+ */
+ private Locale defaultLocale = new Locale("en");
+
+ /**
+ * Where to find caption property files, used in case of CaptionsImpl used.
+ */
+ private String debugCaptionsDir;
+
+ /**
+ * Where to find resources by DebugResourceFilter.
+ */
+ private String debugWebResourcesDir;
+
+ /**
+ * Class name for Captions implementations.
+ */
+ private String captionsImplClass = CaptionsImpl.class.getName();
+
+ /**
+ * Captions implementation instance.
+ */
+ private Captions captions;
+
+ /**
+ * Encoding for caption property files, used in case of CaptionsImpl used.
+ */
+ private String captionFilesEncoding = StandardCharsets.UTF_8.name();
+
+ /**
+ * Allowed languages (use 2-letter codes). Only English by default.
+ */
+ private List allowedLanguages = Collections.singletonList("en");
+
+ /**
+ * To use geoip to setup language by 2-letter uppercase country code (ISO 3166 code).
+ * The property should have a form like: RU,BY:ru;EN,GB,US,CA:en.
+ */
+ private Map countryToLanguage = new HashMap<>();
+
+ /**
+ * Default reset strategy for fields of Components: should they be reset after request processing.
+ */
+ private ResetStrategy resetStrategy;
+
+ /**
+ * Delay between checks of template files to be changed (in seconds).
+ */
+ private int templatesUpdateDelay = 60;
+
+ /**
+ * List of annotation classes to override default strategy, should be used on classes or fields.
+ */
+ private Set resetAnnotations;
+
+ /**
+ * List of annotation classes to override default strategy, should be used on classes or fields.
+ */
+ private Set persistAnnotations;
+
+ /**
+ * Guice injector.
+ */
+ private Injector injector;
+
+ /**
+ * RequestContext for current thread.
+ */
+ private static final ThreadLocal requestsPerThread = new ThreadLocal<>();
+
+ /**
+ * Reloading class loader for current thread, used in debug mode only.
+ */
+ private static final ThreadLocal reloadingClassLoaderPerThread = new ThreadLocal<>();
+
+ /**
+ * Current reloading class loader.
+ */
+ private ClassLoader reloadingClassLoader = getClass().getClassLoader();
+
+ /**
+ * List of loaded modules.
+ */
+ private List modules = new ArrayList<>();
+
+ /**
+ * ApplicationContext is initialized.
+ */
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+
+ private final Lock initializedLock = new ReentrantLock();
+ private final Condition initializedCondition = initializedLock.newCondition();
+
+ void setInitialized() {
+ initializedLock.lock();
+ try {
+ if (!initialized.getAndSet(true)) {
+ initializedCondition.signalAll();
+ }
+ } finally {
+ initializedLock.unlock();
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @Contract(pure = true)
+ boolean isInitialized() {
+ return initialized.get();
+ }
+
+ void setRequestAndResponse(HttpServletRequest request, HttpServletResponse response) {
+ requestsPerThread.set(new RequestContext(request, response));
+ }
+
+ public void unsetRequestAndResponse() {
+ requestsPerThread.set(new RequestContext(null, null));
+ }
+
+ /**
+ * In debug mode it will return reloading class loader, and it
+ * will return typical web-application class loader in production mode.
+ *
+ * @return Reloading class loader (for debug mode) and usual web-application
+ * class loader (for production mode).
+ */
+ public ClassLoader getReloadingClassLoader() {
+ if (debug) {
+ return reloadingClassLoaderPerThread.get();
+ } else {
+ return reloadingClassLoader;
+ }
+ }
+
+ /**
+ * @return Returns application context path.
+ * You should build paths in your application by
+ * concatenation getContextPath() and relative path inside
+ * the application.
+ */
+ public String getContextPath() {
+ if (contextPath == null) {
+ return getRequest().getContextPath();
+ } else {
+ return contextPath;
+ }
+ }
+
+ /**
+ * Where to find captions properties files if
+ * naive org.nocturne.caption.CaptionsImpl backed used and
+ * debug mode switched on.
+ *
+ * @return Directory or null in the production mode.
+ */
+ @Nullable
+ public String getDebugCaptionsDir() {
+ if (debug) {
+ return debugCaptionsDir;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return Default application locale, specified by
+ * nocturne.default-language. English if no one specified.
+ */
+ public Locale getDefaultLocale() {
+ return defaultLocale;
+ }
+
+ /**
+ * @return List of allowed languages, use property nocturne.allowed-languages.
+ */
+ public List getAllowedLanguages() {
+ return Collections.unmodifiableList(allowedLanguages);
+ }
+
+ /**
+ * @return Map to setup language by 2-letter uppercase country code (ISO 3166 code).
+ * The property should have a form like: RU,BY:ru;EN,GB,US,CA:en.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public Map getCountryToLanguage() {
+ return Collections.unmodifiableMap(countryToLanguage);
+ }
+
+ /**
+ * @return Default reset strategy for fields of Components: should they be reset after request processing.
+ */
+ public ResetStrategy getResetStrategy() {
+ return resetStrategy;
+ }
+
+ /**
+ * @return Delay between checks of template files to be changed (in seconds).
+ */
+ public int getTemplatesUpdateDelay() {
+ return templatesUpdateDelay;
+ }
+
+ /**
+ * @return List of annotation classes to override default strategy, should be used on classes or fields.
+ */
+ public Set getResetAnnotations() {
+ return Collections.unmodifiableSet(resetAnnotations);
+ }
+
+ /**
+ * @return List of annotation classes to override default strategy, should be used on classes or fields.
+ */
+ public Set getPersistAnnotations() {
+ return Collections.unmodifiableSet(persistAnnotations);
+ }
+
+ void setTemplatesUpdateDelay(int templatesUpdateDelay) {
+ this.templatesUpdateDelay = templatesUpdateDelay;
+ }
+
+ void setUseComponentTemplates(boolean useComponentTemplates) {
+ this.useComponentTemplates = useComponentTemplates;
+ }
+
+ public boolean isUseComponentTemplates() {
+ return useComponentTemplates;
+ }
+
+ public File getComponentTemplatesLessCommonsFile() {
+ return componentTemplatesLessCommonsFile;
+ }
+
+ void setComponentTemplatesLessCommonsFile(File componentTemplatesLessCommonsFile) {
+ this.componentTemplatesLessCommonsFile = componentTemplatesLessCommonsFile;
+ }
+
+ void setResetStrategy(ResetStrategy resetStrategy) {
+ this.resetStrategy = resetStrategy;
+ }
+
+ void setResetAnnotations(Collection resetAnnotations) {
+ this.resetAnnotations = new LinkedHashSet<>(resetAnnotations);
+ }
+
+ void setPersistAnnotations(Collection persistAnnotations) {
+ this.persistAnnotations = new LinkedHashSet<>(persistAnnotations);
+ }
+
+ /**
+ * @return What page to show if RequestRouter returns {@code null}.
+ * Returns {@code null} if application should return 404 on it.
+ */
+ public String getDefaultPageClassName() {
+ return defaultPageClassName;
+ }
+
+ /**
+ * @return Encoding for caption files if
+ * naive org.nocturne.caption.CaptionsImpl backed used.
+ */
+ public String getCaptionFilesEncoding() {
+ return captionFilesEncoding;
+ }
+
+ /**
+ * @return Current rendering frame or page.
+ */
+ public Component getCurrentComponent() {
+ return currentComponent.get();
+ }
+
+ /**
+ * @return Current rendering page instance.
+ */
+ public Page getCurrentPage() {
+ return currentPage.get();
+ }
+
+ /**
+ * Method to get application context.
+ *
+ * @return The only application context instance.
+ */
+ public static ApplicationContext getInstance() {
+ return INSTANCE;
+ }
+
+ void setContextPath(String contextPath) {
+ this.contextPath = contextPath;
+ }
+
+ void setLink(Link link) {
+ getRequest().setAttribute("nocturne.current-page-link", link);
+ }
+
+ /**
+ * @return Link annotation instance, which was choosen by LinkedRequestRouter as
+ * link for current request.
+ */
+ public Link getLink() {
+ return (Link) getRequest().getAttribute("nocturne.current-page-link");
+ }
+
+ void setDefaultPageClassName(String defaultPageClassName) {
+ this.defaultPageClassName = defaultPageClassName;
+ }
+
+ void setCurrentPage(Page page) {
+ currentPage.set(page);
+ }
+
+ /**
+ * @return Is application in the debug mode?
+ */
+ public boolean isDebug() {
+ return debug;
+ }
+
+ /**
+ * @return Captions implementation class name.
+ */
+ public String getCaptionsImplClass() {
+ return captionsImplClass;
+ }
+
+ void setCurrentComponent(Component component) {
+ currentComponent.set(component);
+ }
+
+ /**
+ * @return List of directories to be scanned for recompiled classes.
+ * Used in the debug mode only.
+ * Possibly, it depends on your IDE.
+ * Setup it by nocturne.reloading-class-paths.
+ */
+ public List getReloadingClassPaths() {
+ return new LinkedList<>(reloadingClassPaths);
+ }
+
+ /**
+ * @return List of listener class names. Setup it by nocturne.page-request-listeners.
+ */
+ public List getPageRequestListeners() {
+ return new LinkedList<>(pageRequestListeners);
+ }
+
+ void addRequestOverrideParameter(String name, String value) {
+ requestsPerThread.get().addOverrideParameter(name, value);
+ }
+
+ void addRequestOverrideParameter(String name, List values) {
+ requestsPerThread.get().addOverrideParameter(name, values);
+ }
+
+ Map> getRequestOverrideParameters() {
+ return requestsPerThread.get().getOverrideParameters();
+ }
+
+ void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ void setReloadingClassPaths(List reloadingClassPaths) {
+ this.reloadingClassPaths = new LinkedHashSet<>(reloadingClassPaths);
+ }
+
+ void setPageRequestListeners(List pageRequestListeners) {
+ this.pageRequestListeners = new LinkedHashSet<>(pageRequestListeners);
+ }
+
+ void setCaptionFilesEncoding(String captionFilesEncoding) {
+ this.captionFilesEncoding = captionFilesEncoding;
+ }
+
+ void setRequestRouter(String requestRouter) {
+ this.requestRouter = requestRouter;
+ }
+
+ void setTemplatePaths(String[] templatePaths) {
+ this.templatePaths = Arrays.copyOf(templatePaths, templatePaths.length);
+ }
+
+ void setDefaultLocale(String defaultLanguage) {
+ this.defaultLocale = new Locale(defaultLanguage.toLowerCase());
+ }
+
+ void setGuiceModuleClassName(String guiceModuleClassName) {
+ this.guiceModuleClassName = guiceModuleClassName;
+ }
+
+ void setClassReloadingExceptions(List classReloadingExceptions) {
+ this.classReloadingExceptions = new LinkedHashSet<>(classReloadingExceptions);
+ }
+
+ /**
+ * @return Returns request router instance. Specify nocturne.request-router
+ * property to set its class name.
+ */
+ public String getRequestRouter() {
+ return requestRouter;
+ }
+
+ /**
+ * @return Guice IoC module class name. Set nocturne.guice-module-class-name property.
+ */
+ public String getGuiceModuleClassName() {
+ return guiceModuleClassName;
+ }
+
+ void setClassReloadingPackages(List classReloadingPackages) {
+ this.classReloadingPackages = new LinkedHashSet<>(classReloadingPackages);
+ }
+
+ void setInjector(Injector injector) {
+ lock.lock();
+ try {
+ this.injector = injector;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * @return Guice injector. It is not good idea to use it.
+ */
+ public Injector getInjector() {
+ return injector;
+ }
+
+ /**
+ * @return List of packages (or classes) which will be reloaded using
+ * ReloadingClassLoader. Set nocturne.class-reloading-packages to specify it.
+ */
+ public List getClassReloadingPackages() {
+ return new LinkedList<>(classReloadingPackages);
+ }
+
+ /**
+ * @return List of packages (or classes) which should not be reloaded
+ * using ReloadingClassLoader, even they are in classReloadingPackages.
+ * Set nocturne.class-reloading-exceptions to specify the value.
+ */
+ public List getClassReloadingExceptions() {
+ return new LinkedList<>(classReloadingExceptions);
+ }
+
+ /**
+ * @return Where to find templates. Contains relative paths from deployed application root. For example: WEB-INF/templates.
+ * Set nocturne.template-paths - semicolon separated list of paths.
+ * Set nocturne.templates-path (deprecated) - single path.
+ */
+ public String[] getTemplatePaths() {
+ return Arrays.copyOf(templatePaths, templatePaths.length);
+ }
+
+ /**
+ * @return Flag that indicates if template loader should stick to last successful template path
+ * or always check template paths in the configured order.
+ * Default value is {@code true}.
+ */
+ public boolean isStickyTemplatePaths() {
+ return stickyTemplatePaths;
+ }
+
+ public void setStickyTemplatePaths(boolean stickyTemplatePaths) {
+ this.stickyTemplatePaths = stickyTemplatePaths;
+ }
+
+ void setAllowedLanguages(List allowedLanguages) {
+ this.allowedLanguages = new ArrayList<>(allowedLanguages);
+ }
+
+ void setCountryToLanguage(Map countryToLanguage) {
+ this.countryToLanguage = new HashMap<>(countryToLanguage);
+ }
+
+ void setServletContext(ServletContext servletContext) {
+ this.servletContext = servletContext;
+ }
+
+ void setDebugCaptionsDir(String debugCaptionsDir) {
+ this.debugCaptionsDir = debugCaptionsDir;
+ }
+
+ /**
+ * @return Returns current servlet context.
+ */
+ public ServletContext getServletContext() {
+ return servletContext;
+ }
+
+
+ /**
+ * @return if request.getServletPath() (example: /some/path) matches it, request ignored by nocturne.
+ * Use nocturne.skip-regex to set it.
+ */
+ public Pattern getSkipRegex() {
+ return skipRegex;
+ }
+
+ void setCaptionsImplClass(String captionsImplClass) {
+ this.captionsImplClass = captionsImplClass;
+ }
+
+ void setSkipRegex(Pattern skipRegex) {
+ this.skipRegex = skipRegex;
+ }
+
+ public boolean hasRequest() {
+ RequestContext requestContext = requestsPerThread.get();
+ return requestContext != null && requestContext.getRequest() != null;
+ }
+
+ /**
+ * @return Returns current servlet request instance.
+ */
+ public HttpServletRequest getRequest() {
+ return requestsPerThread.get().getRequest();
+ }
+
+ public boolean hasResponse() {
+ RequestContext requestContext = requestsPerThread.get();
+ return requestContext != null && requestContext.getResponse() != null;
+ }
+
+ /**
+ * @return Returns current servlet response instance.
+ */
+ public HttpServletResponse getResponse() {
+ return requestsPerThread.get().getResponse();
+ }
+
+ void setReloadingClassLoader(ClassLoader loader) {
+ if (debug) {
+ reloadingClassLoaderPerThread.set(loader);
+ } else {
+ reloadingClassLoader = loader;
+ }
+ }
+
+ /**
+ * Modules use the method to update reloading class path.
+ * Do not use it from your code.
+ *
+ * @param dir Directory to be added.
+ */
+ public void addReloadingClassPath(File dir) {
+ if (!dir.isDirectory()) {
+ logger.error("Path " + dir.getName() + " expected to be a directory.");
+ throw new ConfigurationException("Path " + dir.getName() + " expected to be a directory.");
+ }
+ reloadingClassPaths.add(dir);
+
+ if (debug) {
+ ReloadingContext context = ReloadingContext.getInstance();
+ try {
+ ReflectionUtil.invoke(context, "addReloadingClassPath", dir);
+ } catch (ReflectionException e) {
+ logger.error("Can't call addReloadingClassPath for ReloadingContext.", e);
+ throw new NocturneException("Can't call addReloadingClassPath for ReloadingContext.", e);
+ }
+ } else {
+ ReloadingContext.getInstance().addReloadingClassPath(dir);
+ }
+ }
+
+ public void addClassReloadingException(String packageOrClassName) {
+ if (debug) {
+ classReloadingExceptions.add(packageOrClassName);
+ ReloadingContext.getInstance().addClassReloadingException(packageOrClassName);
+ }
+ }
+
+ /**
+ * @param shortcut Shortcut value.
+ * @return Use the method to work with captions from your code.
+ * Usually, it is not good idea, because captions are part of view layer.
+ */
+ @SuppressWarnings("DollarSignInName")
+ public String $(String shortcut) {
+ return $(shortcut, ArrayUtils.EMPTY_OBJECT_ARRAY);
+ }
+
+ /**
+ * @param shortcut Shortcut value.
+ * @param args Shortcut arguments.
+ * @return Use the method to work with captions from your code.
+ * Usually, it is not good idea, because captions are part of view layer.
+ */
+ @SuppressWarnings({"OverloadedVarargsMethod", "DollarSignInName"})
+ public String $(String shortcut, Object... args) {
+ shortcut = shortcut.trim();
+ initializeCaptions();
+ return captions.find(shortcut, args);
+ }
+
+ /**
+ * @param locale Expected locale.
+ * @param shortcut Shortcut value.
+ * @param args Shortcut arguments.
+ * @return Use the method to work with captions from your code.
+ * Usually, it is not good idea, because captions are part of view layer.
+ */
+ @SuppressWarnings("OverloadedVarargsMethod")
+ public String getCaption(Locale locale, String shortcut, Object... args) {
+ shortcut = shortcut.trim();
+ initializeCaptions();
+ return captions.find(locale, shortcut, args);
+ }
+
+ /**
+ * @param locale Expected locale.
+ * @param shortcut Shortcut value.
+ * @return Use the method to work with captions from your code.
+ * Usually, it is not good idea, because captions are part of view layer.
+ */
+ public String getCaption(Locale locale, String shortcut) {
+ shortcut = shortcut.trim();
+ initializeCaptions();
+ return captions.find(locale, shortcut);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void initializeCaptions() {
+ if (captions != null) {
+ return;
+ }
+
+ lock.lock();
+ try {
+ Class extends Captions> clazz = (Class extends Captions>) getClass().getClassLoader().loadClass(captionsImplClass);
+ captions = injector.getInstance(clazz);
+ } catch (ClassNotFoundException e) {
+ logger.error("Class " + captionsImplClass + " not found.", e);
+ throw new ConfigurationException("Class " + captionsImplClass + " should implement Captions.", e);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * @return Locale for current request.
+ */
+ public Locale getLocale() {
+ return requestsPerThread.get().getLocale();
+ }
+
+ /**
+ * @return List of loaded modules.
+ */
+ public List getModules() {
+ return Collections.unmodifiableList(modules);
+ }
+
+ void setModules(List modules) {
+ this.modules = new ArrayList<>(modules);
+ }
+
+ /**
+ * @return Prefix before attributes in request which
+ * will be injected as parameters in Components.
+ */
+ public static String getAdditionalParamsRequestAttributePrefix() {
+ return "nocturne.additional-parameter.";
+ }
+
+ private static String getActionRequestPageClassName() {
+ return "nocturne.request-page-class-name";
+ }
+
+ private static String getActionRequestParamName() {
+ return "nocturne.request-action";
+ }
+
+ void setRequestAction(String action) {
+ getRequest().setAttribute(getActionRequestParamName(), action);
+ }
+
+ /**
+ * @return Action for current request (how request router decided). Empty string if
+ * no one specified. Typically, gets from action parameter (example: ?action=test)
+ * or link template (example: "user/{action}").
+ */
+ public String getRequestAction() {
+ return (String) getRequest().getAttribute(getActionRequestParamName());
+ }
+
+ void setRequestPageClassName(String pageClassName) {
+ getRequest().setAttribute(getActionRequestPageClassName(), pageClassName);
+ }
+
+ /**
+ * @return Page class name for current request (how request router decided).
+ */
+ public String getRequestPageClassName() {
+ return (String) getRequest().getAttribute(getActionRequestPageClassName());
+ }
+
+ public void setDebugWebResourcesDir(String debugWebResourcesDir) {
+ this.debugWebResourcesDir = debugWebResourcesDir;
+ }
+
+ /**
+ * @return Returns the directory where to find resources by DebugResourceFilter.
+ */
+ public String getDebugWebResourcesDir() {
+ return debugWebResourcesDir;
+ }
+
+ /**
+ * @param runnable Runnable to be executed after ApplicationContext has been initialized.
+ */
+ public void executeAfterInitialization(Runnable runnable) {
+ new Thread(() -> {
+ initializedLock.lock();
+ try {
+ while (!isInitialized()) {
+ try {
+ initializedCondition.await();
+ } catch (InterruptedException ignored) {
+ // No operations.
+ }
+ }
+ runnable.run();
+ } finally {
+ initializedLock.unlock();
+ }
+ }).start();
+ }
+
+ /**
+ * Stores current request context: request, response and locale.
+ */
+ private static final class RequestContext {
+ private static final Pattern ACCEPT_LANGUAGE_SPLIT_PATTERN = Pattern.compile("[,;-]");
+ private static final String LANGUAGE_COOKIE_NAME = "nocturne.language";
+ /**
+ * Http servlet request.
+ */
+ private final HttpServletRequest request;
+
+ /**
+ * Http servlet response.
+ */
+ private final HttpServletResponse response;
+
+ /**
+ * Locale for current request.
+ */
+ private Locale locale;
+
+ /**
+ * Parameters which override request params.
+ */
+ private Map> overrideParameters;
+
+ private RequestContext(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) {
+ if ((request == null) ^ (response == null)) {
+ logger.error("It is not possible case '(request == null) ^ (response == null)'.");
+ throw new IllegalArgumentException("It is not possible case '(request == null) ^ (response == null)'.");
+ }
+
+ this.request = request;
+ this.response = response;
+
+ if (request == null) {
+ this.locale = null;
+ } else {
+ setupLocale();
+ }
+ }
+
+ /**
+ * @return Http servlet request.
+ */
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ /**
+ * @return Http servlet response.
+ */
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ /**
+ * @return Locale for current request.
+ * Uses lang, language, locale parameters to find
+ * current locale: 2-letter language code.
+ * If it specified once, stores current locale in the session.
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ @Contract(value = "null -> true", pure = true)
+ private static boolean isInvalidLanguage(@Nullable String lang) {
+ return lang == null || lang.length() != 2;
+ }
+
+ private void setupLocale() {
+ Map> requestMap = RequestUtil.getRequestParams(request);
+
+ String lang = RequestUtil.getFirst(requestMap, "lang");
+
+ if (isInvalidLanguage(lang)) {
+ lang = RequestUtil.getFirst(requestMap, "language");
+ }
+
+ if (isInvalidLanguage(lang)) {
+ lang = RequestUtil.getFirst(requestMap, "locale");
+ }
+
+ if (isInvalidLanguage(lang)) {
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ lang = (String) session.getAttribute("nocturne.language");
+ }
+
+ if (isInvalidLanguage(lang)) {
+ lang = getCookie(LANGUAGE_COOKIE_NAME);
+ }
+
+ if (isInvalidLanguage(lang)) {
+ lang = getLanguageByGeoIp();
+ if (isInvalidLanguage(lang)) {
+ String[] languages = getAcceptLanguages();
+ for (String language : languages) {
+ if (getInstance().getAllowedLanguages().contains(language)
+ && !"en".equalsIgnoreCase(language)) {
+ lang = language;
+ break;
+ }
+ }
+
+ if (isInvalidLanguage(lang)) {
+ for (String language : languages) {
+ if (getInstance().getAllowedLanguages().contains(language)) {
+ lang = language;
+ break;
+ }
+ }
+ }
+ }
+ }
+ locale = localeByLanguage(lang);
+ } else {
+ locale = localeByLanguage(lang);
+ request.getSession().setAttribute("nocturne.language", locale.getLanguage());
+ addCookie(LANGUAGE_COOKIE_NAME, lang, TimeUnit.DAYS.toSeconds(30));
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ @Nullable
+ private String getCookie(String cookieName) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookieName.equals(cookie.getName())) {
+ return cookie.getValue();
+ }
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private void addCookie(String cookieName, String cookieValue, long maxAge) {
+ boolean updated = false;
+ if (request.getCookies() != null) {
+ for (Cookie cookie : request.getCookies()) {
+ if (cookie.getName().equals(cookieName)) {
+ cookie.setValue(cookieValue);
+ if (updated) {
+ cookie.setMaxAge(0);
+ } else {
+ cookie.setMaxAge(Ints.checkedCast(maxAge));
+ }
+ cookie.setPath("/");
+ response.addCookie(cookie);
+ updated = true;
+ }
+ }
+ }
+
+ if (!updated) {
+ Cookie cookie = new Cookie(cookieName, cookieValue);
+ cookie.setMaxAge(Ints.checkedCast(maxAge));
+ cookie.setPath("/");
+ response.addCookie(cookie);
+ }
+ }
+
+ @Nullable
+ private String getLanguageByGeoIp() {
+ String countryCode = null; // GeoIpUtil.getCountryCode(request);
+ String lang = getInstance().getCountryToLanguage().get(countryCode);
+ String[] languages = getAcceptLanguages();
+
+ if (ArrayUtils.indexOf(languages, lang) >= 0 && getInstance().getAllowedLanguages().contains(lang)) {
+ return lang;
+ }
+
+ return null;
+ }
+
+ private String[] getAcceptLanguages() {
+ String header = request.getHeader("Accept-Language");
+
+ if (StringUtil.isEmpty(header)) {
+ return EMPTY_STRING_ARRAY;
+ } else {
+ String[] result = ACCEPT_LANGUAGE_SPLIT_PATTERN.split(header);
+ for (int i = 0; i < result.length; ++i) {
+ result[i] = result[i].toLowerCase();
+ }
+ return result;
+ }
+ }
+
+ @Nonnull
+ private static Locale localeByLanguage(@Nullable String language) {
+ if (getInstance().getAllowedLanguages().contains(language)) {
+ return new Locale(Preconditions.checkNotNull(language));
+ } else {
+ return getInstance().getDefaultLocale();
+ }
+ }
+
+ private void addOverrideParameter(String name, String value) {
+ if (overrideParameters == null) {
+ overrideParameters = new HashMap<>();
+ }
+
+ overrideParameters.put(name, new SingleEntryList<>(value));
+ }
+
+ private void addOverrideParameter(String name, Collection values) {
+ if (overrideParameters == null) {
+ overrideParameters = new HashMap<>();
+ }
+
+ overrideParameters.put(name, new ArrayList<>(values));
+ }
+
+ private Map> getOverrideParameters() {
+ return overrideParameters;
+ }
+ }
+}
diff --git a/code/src/main/java/org/nocturne/main/ApplicationContextLoader.java b/code/src/main/java/org/nocturne/main/ApplicationContextLoader.java
index c36d9e4..1c65789 100644
--- a/code/src/main/java/org/nocturne/main/ApplicationContextLoader.java
+++ b/code/src/main/java/org/nocturne/main/ApplicationContextLoader.java
@@ -1,574 +1,574 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import com.google.inject.Guice;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Logger;
-import org.nocturne.exception.ConfigurationException;
-import org.nocturne.exception.ModuleInitializationException;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.module.Configuration;
-import org.nocturne.module.Module;
-import org.nocturne.prometheus.Prometheus;
-import org.nocturne.reset.ResetStrategy;
-import org.nocturne.reset.annotation.Persist;
-import org.nocturne.reset.annotation.Reset;
-import org.nocturne.util.StringUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.*;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-
-/**
- * @author Mike Mirzayanov
- */
-class ApplicationContextLoader {
- private static final Logger logger = Logger.getLogger(ApplicationContextLoader.class);
-
- private static final Properties properties = new Properties();
- private static final Pattern ITEMS_SPLIT_PATTERN = Pattern.compile("\\s*;\\s*");
- private static final Pattern LANGUAGES_SPLIT_PATTERN = Pattern.compile("[,;\\s]+");
- private static final Pattern COUNTRIES_TO_LANGUAGE_PATTERN = Pattern.compile("([A-Z]{2},)*[A-Z]{2}:[a-z]{2}");
-
- private static void run() {
- setupDebug();
- setupTemplates();
-
- if (ApplicationContext.getInstance().isDebug()) {
- setupReloadingClassPaths();
- setupClassReloadingPackages();
- setupClassReloadingExceptions();
- setupDebugCaptionsDir();
- setupDebugWebResourcesDir();
- }
-
- setupPageRequestListeners();
- setupGuiceModuleClassName();
- setupSkipRegex();
- setupRequestRouter();
- setupDefaultLocale();
- setupCaptionsImplClass();
- setupCaptionFilesEncoding();
- setupAllowedLanguages();
- setupCountryToLanguage();
- setupDefaultPageClassName();
- setupContextPath();
- setupResetProperties();
- }
-
- private static void setupResetProperties() {
- String strategy = properties.getProperty("nocturne.reset.strategy");
- if (StringUtil.isEmpty(strategy)) {
- ApplicationContext.getInstance().setResetStrategy(ResetStrategy.PERSIST);
- } else {
- ApplicationContext.getInstance().setResetStrategy(ResetStrategy.valueOf(strategy));
- }
-
- String resetAnnotations = properties.getProperty("nocturne.reset.reset-annotations");
- if (StringUtil.isEmpty(resetAnnotations)) {
- ApplicationContext.getInstance().setResetAnnotations(Collections.singletonList(Reset.class.getName()));
- } else {
- String[] annotations = ITEMS_SPLIT_PATTERN.split(resetAnnotations);
- ApplicationContext.getInstance().setResetAnnotations(Arrays.asList(annotations));
- }
-
- String persistAnnotations = properties.getProperty("nocturne.reset.persist-annotations");
- if (StringUtil.isEmpty(persistAnnotations)) {
- ApplicationContext.getInstance().setPersistAnnotations(Arrays.asList(
- Persist.class.getName(),
- Inject.class.getName()
- ));
- } else {
- String[] annotations = ITEMS_SPLIT_PATTERN.split(persistAnnotations);
- ApplicationContext.getInstance().setPersistAnnotations(Arrays.asList(annotations));
- }
- }
-
- private static void setupContextPath() {
- if (properties.containsKey("nocturne.context-path")) {
- String contextPath = properties.getProperty("nocturne.context-path");
- if (contextPath != null) {
- ApplicationContext.getInstance().setContextPath(contextPath);
- }
- }
- }
-
- private static void setupDefaultPageClassName() {
- if (properties.containsKey("nocturne.default-page-class-name")) {
- String className = properties.getProperty("nocturne.default-page-class-name");
- if (className != null && !className.isEmpty()) {
- ApplicationContext.getInstance().setDefaultPageClassName(className);
- }
- }
- }
-
- private static void setupAllowedLanguages() {
- if (properties.containsKey("nocturne.allowed-languages")) {
- String languages = properties.getProperty("nocturne.allowed-languages");
- if (languages != null && !languages.isEmpty()) {
- String[] tokens = LANGUAGES_SPLIT_PATTERN.split(languages);
- List list = new ArrayList<>();
- for (String token : tokens) {
- if (!token.isEmpty()) {
- if (token.length() != 2) {
- logger.error("nocturne.allowed-languages should contain the " +
- "list of 2-letters language codes separated with comma.");
- throw new ConfigurationException("nocturne.allowed-languages should contain the " +
- "list of 2-letters language codes separated with comma.");
- }
- list.add(token);
- }
- }
- ApplicationContext.getInstance().setAllowedLanguages(list);
- }
- }
- }
-
- private static void setupCountryToLanguage() {
- if (properties.containsKey("nocturne.countries-to-language")) {
- String countriesToLanguage = properties.getProperty("nocturne.countries-to-language");
- if (countriesToLanguage != null && !countriesToLanguage.isEmpty()) {
- String[] tokens = ITEMS_SPLIT_PATTERN.split(countriesToLanguage);
- Map result = new HashMap<>();
- for (String token : tokens) {
- if (!token.isEmpty()) {
- if (!COUNTRIES_TO_LANGUAGE_PATTERN.matcher(token).matches()) {
- logger.error("nocturne.countries-to-language should have a form like " +
- "\"RU,BY:ru;EN,GB,US,CA:en\".");
- throw new ConfigurationException("nocturne.countries-to-language should have a form like " +
- "\"RU,BY:ru;EN,GB,US,CA:en\".");
- }
- String[] countriesAndLanguage = token.split(":");
- String[] countries = countriesAndLanguage[0].split(",");
- for (String country : countries) {
- result.put(country, countriesAndLanguage[1]);
- }
- }
- }
- ApplicationContext.getInstance().setCountryToLanguage(result);
- }
- }
- }
-
- private static void setupCaptionFilesEncoding() {
- if (properties.containsKey("nocturne.caption-files-encoding")) {
- String encoding = properties.getProperty("nocturne.caption-files-encoding");
- if (encoding != null && !encoding.isEmpty()) {
- ApplicationContext.getInstance().setCaptionFilesEncoding(encoding);
- }
- }
- }
-
- private static void setupCaptionsImplClass() {
- if (properties.containsKey("nocturne.captions-impl-class")) {
- String clazz = properties.getProperty("nocturne.captions-impl-class");
- if (clazz != null && !clazz.isEmpty()) {
- ApplicationContext.getInstance().setCaptionsImplClass(clazz);
- }
- }
- }
-
- private static void setupDebugCaptionsDir() {
- if (properties.containsKey("nocturne.debug-captions-dir")) {
- String dir = properties.getProperty("nocturne.debug-captions-dir");
- if (dir != null && !dir.isEmpty()) {
- if (!new File(dir).isDirectory() && ApplicationContext.getInstance().isDebug()) {
- logger.error("nocturne.debug-captions-dir property should be a directory.");
- throw new ConfigurationException("nocturne.debug-captions-dir property should be a directory.");
- }
- ApplicationContext.getInstance().setDebugCaptionsDir(dir);
- }
- }
- }
-
- private static void setupDefaultLocale() {
- if (properties.containsKey("nocturne.default-language")) {
- String language = properties.getProperty("nocturne.default-language");
- if (language != null && !language.isEmpty()) {
- if (language.length() != 2) {
- logger.error("Language is expected to have exactly two letters.");
- throw new ConfigurationException("Language is expected to have exactly two letters.");
- }
- ApplicationContext.getInstance().setDefaultLocale(language);
- }
- }
- }
-
- private static void setupRequestRouter() {
- if (properties.containsKey("nocturne.request-router")) {
- String resolver = properties.getProperty("nocturne.request-router");
- if (resolver == null || resolver.isEmpty()) {
- logger.error("Parameter nocturne.request-router can't be empty.");
- throw new ConfigurationException("Parameter nocturne.request-router can't be empty.");
- }
- ApplicationContext.getInstance().setRequestRouter(resolver);
- } else {
- logger.error("Missed parameter nocturne.request-router.");
- throw new ConfigurationException("Missed parameter nocturne.request-router.");
- }
- }
-
- private static void setupDebugWebResourcesDir() {
- if (properties.containsKey("nocturne.debug-web-resources-dir")) {
- String dir = properties.getProperty("nocturne.debug-web-resources-dir");
- if (dir != null && !dir.trim().isEmpty()) {
- ApplicationContext.getInstance().setDebugWebResourcesDir(dir.trim());
- }
- }
- }
-
- private static void setupClassReloadingExceptions() {
- List exceptions = new ArrayList<>();
- exceptions.add(ApplicationContext.class.getName());
- exceptions.add(Prometheus.class.getName());
- if (properties.containsKey("nocturne.class-reloading-exceptions")) {
- String exceptionsAsString = properties.getProperty("nocturne.class-reloading-exceptions");
- if (exceptionsAsString != null) {
- exceptions.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(exceptionsAsString)));
- }
- }
- ApplicationContext.getInstance().setClassReloadingExceptions(exceptions);
- }
-
- private static void setupClassReloadingPackages() {
- List packages = new ArrayList<>();
- packages.add("org.nocturne");
-
- if (properties.containsKey("nocturne.class-reloading-packages")) {
- String packagesAsString = properties.getProperty("nocturne.class-reloading-packages");
- if (packagesAsString != null) {
- packages.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(packagesAsString)));
- }
- }
- ApplicationContext.getInstance().setClassReloadingPackages(packages);
- }
-
- private static void setupSkipRegex() {
- if (properties.containsKey("nocturne.skip-regex")) {
- String regex = properties.getProperty("nocturne.skip-regex");
- if (regex != null && !regex.isEmpty()) {
- try {
- ApplicationContext.getInstance().setSkipRegex(Pattern.compile(regex));
- } catch (PatternSyntaxException e) {
- logger.error("Parameter nocturne.skip-regex contains invalid pattern.", e);
- throw new ConfigurationException("Parameter nocturne.skip-regex contains invalid pattern.", e);
- }
- }
- }
- }
-
- private static void setupGuiceModuleClassName() {
- if (properties.containsKey("nocturne.guice-module-class-name")) {
- String module = properties.getProperty("nocturne.guice-module-class-name");
- if (module != null && !module.isEmpty()) {
- ApplicationContext.getInstance().setGuiceModuleClassName(module);
- }
- }
- }
-
- private static void setupPageRequestListeners() {
- List listeners = new ArrayList<>();
- if (properties.containsKey("nocturne.page-request-listeners")) {
- String pageRequestListenersAsString = properties.getProperty("nocturne.page-request-listeners");
- if (pageRequestListenersAsString != null) {
- listeners.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(pageRequestListenersAsString)));
- }
- }
- ApplicationContext.getInstance().setPageRequestListeners(listeners);
- }
-
- private static void setupReloadingClassPaths() {
- List reloadingClassPaths = new ArrayList<>();
- if (properties.containsKey("nocturne.reloading-class-paths")) {
- String reloadingClassPathsAsString = properties.getProperty("nocturne.reloading-class-paths");
- if (reloadingClassPathsAsString != null) {
- String[] dirs = ITEMS_SPLIT_PATTERN.split(reloadingClassPathsAsString);
- for (String dir : dirs) {
- if (dir != null && !dir.isEmpty()) {
- File file = new File(dir);
- if (!file.isDirectory() && ApplicationContext.getInstance().isDebug()) {
- logger.error("Each item in nocturne.reloading-class-paths should be a directory,"
- + " but " + file + " is not.");
- throw new ConfigurationException("Each item in nocturne.reloading-class-paths should be a directory,"
- + " but " + file + " is not.");
- }
- reloadingClassPaths.add(file);
- }
- }
- }
- }
- ApplicationContext.getInstance().setReloadingClassPaths(reloadingClassPaths);
- }
-
- private static void setupTemplates() {
- if (properties.containsKey("nocturne.templates-update-delay")) {
- try {
- int templatesUpdateDelay = Integer.parseInt(properties.getProperty("nocturne.templates-update-delay"));
- if (templatesUpdateDelay < 0 || templatesUpdateDelay > 86400) {
- logger.error("Parameter nocturne.templates-update-delay should be non-negative integer not greater than 86400.");
- throw new ConfigurationException("Parameter nocturne.templates-update-delay should be non-negative integer not greater than 86400.");
- }
- ApplicationContext.getInstance().setTemplatesUpdateDelay(templatesUpdateDelay);
- } catch (NumberFormatException e) {
- logger.error("Parameter nocturne.templates-update-delay should be integer.", e);
- throw new ConfigurationException("Parameter nocturne.templates-update-delay should be integer.", e);
- }
- }
-
- if (properties.containsKey("nocturne.template-paths")) {
- String[] templatePaths = ITEMS_SPLIT_PATTERN.split(StringUtils.trimToEmpty(
- properties.getProperty("nocturne.template-paths")
- ));
-
- for (String templatePath : templatePaths) {
- if (templatePath.isEmpty()) {
- logger.error("Item of parameter nocturne.template-paths can't be empty.");
- throw new ConfigurationException("Item of parameter nocturne.template-paths can't be empty.");
- }
- }
- ApplicationContext.getInstance().setTemplatePaths(templatePaths);
- } else if (properties.containsKey("nocturne.templates-path")) {
- logger.warn("Property nocturne.templates-path is deprecated. Use semicolon separated nocturne.template-paths.");
-
- String templatesPath = StringUtils.trimToEmpty(properties.getProperty("nocturne.templates-path"));
- if (templatesPath.isEmpty()) {
- logger.error("Parameter nocturne.templates-path can't be empty.");
- throw new ConfigurationException("Parameter nocturne.templates-path can't be empty.");
- }
- ApplicationContext.getInstance().setTemplatePaths(new String[]{templatesPath});
- } else {
- logger.error("Missing parameter nocturne.template-paths.");
- throw new ConfigurationException("Missing parameter nocturne.template-paths.");
- }
-
- if (properties.containsKey("nocturne.sticky-template-paths")) {
- String stickyTemplatePaths = StringUtils.trimToEmpty(properties.getProperty("nocturne.sticky-template-paths"));
- if (!stickyTemplatePaths.isEmpty()) {
- ApplicationContext.getInstance().setStickyTemplatePaths(Boolean.parseBoolean(stickyTemplatePaths));
- }
- }
-
- if (properties.containsKey("nocturne.use-component-templates")) {
- String useComponentTemplates = properties.getProperty("nocturne.use-component-templates");
- if (!"false".equals(useComponentTemplates) && !"true".equals(useComponentTemplates)) {
- logger.error("Parameter nocturne.use-component-templates expected to be 'false' or 'true'.");
- throw new ConfigurationException("Parameter nocturne.use-component-templates expected to be 'false' or 'true'.");
- }
- boolean use = "true".equals(useComponentTemplates);
- ApplicationContext.getInstance().setUseComponentTemplates(use);
- if (use && properties.containsKey("nocturne.component-templates-less-commons-file")) {
- String componentTemplatesLessCommonsFileAsString
- = properties.getProperty("nocturne.component-templates-less-commons-file");
- if (!StringUtil.isBlank(componentTemplatesLessCommonsFileAsString)) {
- File componentTemplatesLessCommonsFile = new File(componentTemplatesLessCommonsFileAsString);
- if (componentTemplatesLessCommonsFile.isFile()) {
- ApplicationContext.getInstance().setComponentTemplatesLessCommonsFile(componentTemplatesLessCommonsFile);
- } else {
- logger.error("Parameter nocturne.component-templates-less-commons-file is expected to be a file.");
- throw new ConfigurationException("Parameter nocturne.component-templates-less-commons-file is expected to be a file.");
- }
- }
- }
- }
- }
-
- private static void setupDebug() {
- ApplicationContext.getInstance().setDebug(Boolean.parseBoolean(properties.getProperty("nocturne.debug")));
- }
-
- private static List listOfNonEmpties(String[] strings) {
- List result = new ArrayList<>(strings.length);
- for (String s : strings) {
- if (!StringUtil.isEmpty(s)) {
- result.add(s);
- }
- }
- return result;
- }
-
- /**
- * Scans classpath for modules.
- *
- * @return List of modules ordered by priority (from high priority to low).
- */
- private static List getModulesFromClasspath() {
- List modules = new ArrayList<>();
- URLClassLoader loader = (URLClassLoader) ApplicationContext.class.getClassLoader();
- URL[] classPath = loader.getURLs();
- for (URL url : classPath) {
- if (Module.isModuleUrl(url)) {
- modules.add(new Module(url));
- }
- }
- return modules;
- }
-
- /**
- * Runs init() method for all modules.
- * Each module should be initialized on the application startup.
- */
- private static void initializeModules() {
- List modules = getModulesFromClasspath();
-
- for (Module module : modules) {
- module.init();
- }
-
- modules.sort((moduleA, moduleB) -> {
- int priorityComparisonResult = Integer.compare(moduleB.getPriority(), moduleA.getPriority());
- if (priorityComparisonResult != 0) {
- return priorityComparisonResult;
- }
-
- return moduleA.getName().compareTo(moduleB.getName());
- });
-
- for (Module module : modules) {
- module.getConfiguration().addPages();
- }
-
- ApplicationContext.getInstance().setModules(modules);
- }
-
- private static void setupInjector() {
- String guiceModuleClassName = ApplicationContext.getInstance().getGuiceModuleClassName();
- GenericIocModule module = new GenericIocModule();
-
- if (!StringUtil.isEmpty(guiceModuleClassName)) {
- try {
- module.setModule(getApplicationModule(guiceModuleClassName));
- } catch (Exception e) {
- logger.error("Can't load application Guice module.", e);
- throw new ConfigurationException("Can't load application Guice module.", e);
- }
- }
-
- Injector injector = Guice.createInjector(module);
-
- if (ApplicationContext.getInstance().isDebug()) {
- try {
- Method method = ApplicationContext.class.getDeclaredMethod("setInjector", Injector.class);
- method.setAccessible(true);
- method.invoke(ApplicationContext.getInstance(), injector);
- } catch (NoSuchMethodException e) {
- logger.error("Can't find method setInjector.", e);
- throw new NocturneException("Can't find method setInjector.", e);
- } catch (InvocationTargetException e) {
- logger.error("InvocationTargetException", e);
- throw new NocturneException("InvocationTargetException", e);
- } catch (IllegalAccessException e) {
- logger.error("IllegalAccessException", e);
- throw new NocturneException("IllegalAccessException", e);
- }
- } else {
- ApplicationContext.getInstance().setInjector(injector);
- }
- }
-
- private static com.google.inject.Module getApplicationModule(String guiceModuleClassName) throws Exception {
- Class> moduleClass = ApplicationContext.class.getClassLoader().loadClass(guiceModuleClassName);
- AtomicReference exception = new AtomicReference<>();
-
- try {
- return (com.google.inject.Module) moduleClass.getConstructor().newInstance();
- } catch (Exception e) {
- exception.compareAndSet(null, e);
- }
-
- try {
- Method getInstanceMethod = moduleClass.getMethod("getInstance");
- if (Modifier.isStatic(getInstanceMethod.getModifiers())
- && com.google.inject.Module.class.isAssignableFrom(getInstanceMethod.getReturnType())) {
- return (com.google.inject.Module) getInstanceMethod.invoke(null);
- }
- } catch (Exception e) {
- exception.compareAndSet(null, e);
- }
-
- try {
- Method createInstanceMethod = moduleClass.getMethod("createInstance");
- if (Modifier.isStatic(createInstanceMethod.getModifiers())
- && com.google.inject.Module.class.isAssignableFrom(createInstanceMethod.getReturnType())) {
- return (com.google.inject.Module) createInstanceMethod.invoke(null);
- }
- } catch (Exception e) {
- exception.compareAndSet(null, e);
- }
-
- try {
- Method newInstanceMethod = moduleClass.getMethod("newInstance");
- if (Modifier.isStatic(newInstanceMethod.getModifiers())
- && com.google.inject.Module.class.isAssignableFrom(newInstanceMethod.getReturnType())) {
- return (com.google.inject.Module) newInstanceMethod.invoke(null);
- }
- } catch (Exception e) {
- exception.compareAndSet(null, e);
- }
-
- throw exception.get();
- }
-
- private static void runModuleStartups() {
- List modules = ApplicationContext.getInstance().getModules();
- for (Module module : modules) {
- String startupClassName = module.getStartupClassName();
- if (!startupClassName.isEmpty()) {
- Runnable runnable;
- try {
- runnable = (Runnable) ApplicationContext.getInstance().getInjector().getInstance(
- ApplicationContext.class.getClassLoader().loadClass(startupClassName));
- } catch (ClassCastException e) {
- logger.error("Startup class " + startupClassName + " must implement Runnable.", e);
- throw new ModuleInitializationException("Startup class " + startupClassName
- + " must implement Runnable.", e);
- } catch (ClassNotFoundException e) {
- logger.error("Can't load startup class be name " + startupClassName + '.', e);
- throw new ModuleInitializationException("Can't load startup class be name "
- + startupClassName + '.', e);
- }
- if (runnable != null) {
- runnable.run();
- }
- }
- }
- }
-
- static void initialize() {
- synchronized (ApplicationContextLoader.class) {
- run();
- initializeModules();
- setupInjector();
- runModuleStartups();
- ApplicationContext.getInstance().setInitialized();
- }
- }
-
- static void shutdown() {
- synchronized (ApplicationContextLoader.class) {
- ApplicationContext.getInstance().getModules().parallelStream()
- .map(Module::getConfiguration).filter(Objects::nonNull).forEach(Configuration::shutdown);
- }
- }
-
- static {
- try (InputStream inputStream = ApplicationContextLoader.class.getResourceAsStream(Constants.CONFIGURATION_FILE)) {
- properties.load(inputStream);
- } catch (IOException e) {
- logger.error("Can't load resource file " + Constants.CONFIGURATION_FILE + '.', e);
- throw new ConfigurationException("Can't load resource file " + Constants.CONFIGURATION_FILE + '.', e);
- }
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.main;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+import org.nocturne.exception.ConfigurationException;
+import org.nocturne.exception.ModuleInitializationException;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.module.Configuration;
+import org.nocturne.module.Module;
+import org.nocturne.prometheus.Prometheus;
+import org.nocturne.reset.ResetStrategy;
+import org.nocturne.reset.annotation.Persist;
+import org.nocturne.reset.annotation.Reset;
+import org.nocturne.util.StringUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * @author Mike Mirzayanov
+ */
+class ApplicationContextLoader {
+ private static final Logger logger = Logger.getLogger(ApplicationContextLoader.class);
+
+ private static final Properties properties = new Properties();
+ private static final Pattern ITEMS_SPLIT_PATTERN = Pattern.compile("\\s*;\\s*");
+ private static final Pattern LANGUAGES_SPLIT_PATTERN = Pattern.compile("[,;\\s]+");
+ private static final Pattern COUNTRIES_TO_LANGUAGE_PATTERN = Pattern.compile("([A-Z]{2},)*[A-Z]{2}:[a-z]{2}");
+
+ private static void run() {
+ setupDebug();
+ setupTemplates();
+
+ if (ApplicationContext.getInstance().isDebug()) {
+ setupReloadingClassPaths();
+ setupClassReloadingPackages();
+ setupClassReloadingExceptions();
+ setupDebugCaptionsDir();
+ setupDebugWebResourcesDir();
+ }
+
+ setupPageRequestListeners();
+ setupGuiceModuleClassName();
+ setupSkipRegex();
+ setupRequestRouter();
+ setupDefaultLocale();
+ setupCaptionsImplClass();
+ setupCaptionFilesEncoding();
+ setupAllowedLanguages();
+ setupCountryToLanguage();
+ setupDefaultPageClassName();
+ setupContextPath();
+ setupResetProperties();
+ }
+
+ private static void setupResetProperties() {
+ String strategy = properties.getProperty("nocturne.reset.strategy");
+ if (StringUtil.isEmpty(strategy)) {
+ ApplicationContext.getInstance().setResetStrategy(ResetStrategy.PERSIST);
+ } else {
+ ApplicationContext.getInstance().setResetStrategy(ResetStrategy.valueOf(strategy));
+ }
+
+ String resetAnnotations = properties.getProperty("nocturne.reset.reset-annotations");
+ if (StringUtil.isEmpty(resetAnnotations)) {
+ ApplicationContext.getInstance().setResetAnnotations(Collections.singletonList(Reset.class.getName()));
+ } else {
+ String[] annotations = ITEMS_SPLIT_PATTERN.split(resetAnnotations);
+ ApplicationContext.getInstance().setResetAnnotations(Arrays.asList(annotations));
+ }
+
+ String persistAnnotations = properties.getProperty("nocturne.reset.persist-annotations");
+ if (StringUtil.isEmpty(persistAnnotations)) {
+ ApplicationContext.getInstance().setPersistAnnotations(Arrays.asList(
+ Persist.class.getName(),
+ Inject.class.getName()
+ ));
+ } else {
+ String[] annotations = ITEMS_SPLIT_PATTERN.split(persistAnnotations);
+ ApplicationContext.getInstance().setPersistAnnotations(Arrays.asList(annotations));
+ }
+ }
+
+ private static void setupContextPath() {
+ if (properties.containsKey("nocturne.context-path")) {
+ String contextPath = properties.getProperty("nocturne.context-path");
+ if (contextPath != null) {
+ ApplicationContext.getInstance().setContextPath(contextPath);
+ }
+ }
+ }
+
+ private static void setupDefaultPageClassName() {
+ if (properties.containsKey("nocturne.default-page-class-name")) {
+ String className = properties.getProperty("nocturne.default-page-class-name");
+ if (className != null && !className.isEmpty()) {
+ ApplicationContext.getInstance().setDefaultPageClassName(className);
+ }
+ }
+ }
+
+ private static void setupAllowedLanguages() {
+ if (properties.containsKey("nocturne.allowed-languages")) {
+ String languages = properties.getProperty("nocturne.allowed-languages");
+ if (languages != null && !languages.isEmpty()) {
+ String[] tokens = LANGUAGES_SPLIT_PATTERN.split(languages);
+ List list = new ArrayList<>();
+ for (String token : tokens) {
+ if (!token.isEmpty()) {
+ if (token.length() != 2) {
+ logger.error("nocturne.allowed-languages should contain the " +
+ "list of 2-letters language codes separated with comma.");
+ throw new ConfigurationException("nocturne.allowed-languages should contain the " +
+ "list of 2-letters language codes separated with comma.");
+ }
+ list.add(token);
+ }
+ }
+ ApplicationContext.getInstance().setAllowedLanguages(list);
+ }
+ }
+ }
+
+ private static void setupCountryToLanguage() {
+ if (properties.containsKey("nocturne.countries-to-language")) {
+ String countriesToLanguage = properties.getProperty("nocturne.countries-to-language");
+ if (countriesToLanguage != null && !countriesToLanguage.isEmpty()) {
+ String[] tokens = ITEMS_SPLIT_PATTERN.split(countriesToLanguage);
+ Map result = new HashMap<>();
+ for (String token : tokens) {
+ if (!token.isEmpty()) {
+ if (!COUNTRIES_TO_LANGUAGE_PATTERN.matcher(token).matches()) {
+ logger.error("nocturne.countries-to-language should have a form like " +
+ "\"RU,BY:ru;EN,GB,US,CA:en\".");
+ throw new ConfigurationException("nocturne.countries-to-language should have a form like " +
+ "\"RU,BY:ru;EN,GB,US,CA:en\".");
+ }
+ String[] countriesAndLanguage = token.split(":");
+ String[] countries = countriesAndLanguage[0].split(",");
+ for (String country : countries) {
+ result.put(country, countriesAndLanguage[1]);
+ }
+ }
+ }
+ ApplicationContext.getInstance().setCountryToLanguage(result);
+ }
+ }
+ }
+
+ private static void setupCaptionFilesEncoding() {
+ if (properties.containsKey("nocturne.caption-files-encoding")) {
+ String encoding = properties.getProperty("nocturne.caption-files-encoding");
+ if (encoding != null && !encoding.isEmpty()) {
+ ApplicationContext.getInstance().setCaptionFilesEncoding(encoding);
+ }
+ }
+ }
+
+ private static void setupCaptionsImplClass() {
+ if (properties.containsKey("nocturne.captions-impl-class")) {
+ String clazz = properties.getProperty("nocturne.captions-impl-class");
+ if (clazz != null && !clazz.isEmpty()) {
+ ApplicationContext.getInstance().setCaptionsImplClass(clazz);
+ }
+ }
+ }
+
+ private static void setupDebugCaptionsDir() {
+ if (properties.containsKey("nocturne.debug-captions-dir")) {
+ String dir = properties.getProperty("nocturne.debug-captions-dir");
+ if (dir != null && !dir.isEmpty()) {
+ if (!new File(dir).isDirectory() && ApplicationContext.getInstance().isDebug()) {
+ logger.error("nocturne.debug-captions-dir property should be a directory.");
+ throw new ConfigurationException("nocturne.debug-captions-dir property should be a directory.");
+ }
+ ApplicationContext.getInstance().setDebugCaptionsDir(dir);
+ }
+ }
+ }
+
+ private static void setupDefaultLocale() {
+ if (properties.containsKey("nocturne.default-language")) {
+ String language = properties.getProperty("nocturne.default-language");
+ if (language != null && !language.isEmpty()) {
+ if (language.length() != 2) {
+ logger.error("Language is expected to have exactly two letters.");
+ throw new ConfigurationException("Language is expected to have exactly two letters.");
+ }
+ ApplicationContext.getInstance().setDefaultLocale(language);
+ }
+ }
+ }
+
+ private static void setupRequestRouter() {
+ if (properties.containsKey("nocturne.request-router")) {
+ String resolver = properties.getProperty("nocturne.request-router");
+ if (resolver == null || resolver.isEmpty()) {
+ logger.error("Parameter nocturne.request-router can't be empty.");
+ throw new ConfigurationException("Parameter nocturne.request-router can't be empty.");
+ }
+ ApplicationContext.getInstance().setRequestRouter(resolver);
+ } else {
+ logger.error("Missed parameter nocturne.request-router.");
+ throw new ConfigurationException("Missed parameter nocturne.request-router.");
+ }
+ }
+
+ private static void setupDebugWebResourcesDir() {
+ if (properties.containsKey("nocturne.debug-web-resources-dir")) {
+ String dir = properties.getProperty("nocturne.debug-web-resources-dir");
+ if (dir != null && !dir.trim().isEmpty()) {
+ ApplicationContext.getInstance().setDebugWebResourcesDir(dir.trim());
+ }
+ }
+ }
+
+ private static void setupClassReloadingExceptions() {
+ List exceptions = new ArrayList<>();
+ exceptions.add(ApplicationContext.class.getName());
+ exceptions.add(Prometheus.class.getName());
+ if (properties.containsKey("nocturne.class-reloading-exceptions")) {
+ String exceptionsAsString = properties.getProperty("nocturne.class-reloading-exceptions");
+ if (exceptionsAsString != null) {
+ exceptions.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(exceptionsAsString)));
+ }
+ }
+ ApplicationContext.getInstance().setClassReloadingExceptions(exceptions);
+ }
+
+ private static void setupClassReloadingPackages() {
+ List packages = new ArrayList<>();
+ packages.add("org.nocturne");
+
+ if (properties.containsKey("nocturne.class-reloading-packages")) {
+ String packagesAsString = properties.getProperty("nocturne.class-reloading-packages");
+ if (packagesAsString != null) {
+ packages.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(packagesAsString)));
+ }
+ }
+ ApplicationContext.getInstance().setClassReloadingPackages(packages);
+ }
+
+ private static void setupSkipRegex() {
+ if (properties.containsKey("nocturne.skip-regex")) {
+ String regex = properties.getProperty("nocturne.skip-regex");
+ if (regex != null && !regex.isEmpty()) {
+ try {
+ ApplicationContext.getInstance().setSkipRegex(Pattern.compile(regex));
+ } catch (PatternSyntaxException e) {
+ logger.error("Parameter nocturne.skip-regex contains invalid pattern.", e);
+ throw new ConfigurationException("Parameter nocturne.skip-regex contains invalid pattern.", e);
+ }
+ }
+ }
+ }
+
+ private static void setupGuiceModuleClassName() {
+ if (properties.containsKey("nocturne.guice-module-class-name")) {
+ String module = properties.getProperty("nocturne.guice-module-class-name");
+ if (module != null && !module.isEmpty()) {
+ ApplicationContext.getInstance().setGuiceModuleClassName(module);
+ }
+ }
+ }
+
+ private static void setupPageRequestListeners() {
+ List listeners = new ArrayList<>();
+ if (properties.containsKey("nocturne.page-request-listeners")) {
+ String pageRequestListenersAsString = properties.getProperty("nocturne.page-request-listeners");
+ if (pageRequestListenersAsString != null) {
+ listeners.addAll(listOfNonEmpties(ITEMS_SPLIT_PATTERN.split(pageRequestListenersAsString)));
+ }
+ }
+ ApplicationContext.getInstance().setPageRequestListeners(listeners);
+ }
+
+ private static void setupReloadingClassPaths() {
+ List reloadingClassPaths = new ArrayList<>();
+ if (properties.containsKey("nocturne.reloading-class-paths")) {
+ String reloadingClassPathsAsString = properties.getProperty("nocturne.reloading-class-paths");
+ if (reloadingClassPathsAsString != null) {
+ String[] dirs = ITEMS_SPLIT_PATTERN.split(reloadingClassPathsAsString);
+ for (String dir : dirs) {
+ if (dir != null && !dir.isEmpty()) {
+ File file = new File(dir);
+ if (!file.isDirectory() && ApplicationContext.getInstance().isDebug()) {
+ logger.error("Each item in nocturne.reloading-class-paths should be a directory,"
+ + " but " + file + " is not.");
+ throw new ConfigurationException("Each item in nocturne.reloading-class-paths should be a directory,"
+ + " but " + file + " is not.");
+ }
+ reloadingClassPaths.add(file);
+ }
+ }
+ }
+ }
+ ApplicationContext.getInstance().setReloadingClassPaths(reloadingClassPaths);
+ }
+
+ private static void setupTemplates() {
+ if (properties.containsKey("nocturne.templates-update-delay")) {
+ try {
+ int templatesUpdateDelay = Integer.parseInt(properties.getProperty("nocturne.templates-update-delay"));
+ if (templatesUpdateDelay < 0 || templatesUpdateDelay > 86400) {
+ logger.error("Parameter nocturne.templates-update-delay should be non-negative integer not greater than 86400.");
+ throw new ConfigurationException("Parameter nocturne.templates-update-delay should be non-negative integer not greater than 86400.");
+ }
+ ApplicationContext.getInstance().setTemplatesUpdateDelay(templatesUpdateDelay);
+ } catch (NumberFormatException e) {
+ logger.error("Parameter nocturne.templates-update-delay should be integer.", e);
+ throw new ConfigurationException("Parameter nocturne.templates-update-delay should be integer.", e);
+ }
+ }
+
+ if (properties.containsKey("nocturne.template-paths")) {
+ String[] templatePaths = ITEMS_SPLIT_PATTERN.split(StringUtils.trimToEmpty(
+ properties.getProperty("nocturne.template-paths")
+ ));
+
+ for (String templatePath : templatePaths) {
+ if (templatePath.isEmpty()) {
+ logger.error("Item of parameter nocturne.template-paths can't be empty.");
+ throw new ConfigurationException("Item of parameter nocturne.template-paths can't be empty.");
+ }
+ }
+ ApplicationContext.getInstance().setTemplatePaths(templatePaths);
+ } else if (properties.containsKey("nocturne.templates-path")) {
+ logger.warn("Property nocturne.templates-path is deprecated. Use semicolon separated nocturne.template-paths.");
+
+ String templatesPath = StringUtils.trimToEmpty(properties.getProperty("nocturne.templates-path"));
+ if (templatesPath.isEmpty()) {
+ logger.error("Parameter nocturne.templates-path can't be empty.");
+ throw new ConfigurationException("Parameter nocturne.templates-path can't be empty.");
+ }
+ ApplicationContext.getInstance().setTemplatePaths(new String[]{templatesPath});
+ } else {
+ logger.error("Missing parameter nocturne.template-paths.");
+ throw new ConfigurationException("Missing parameter nocturne.template-paths.");
+ }
+
+ if (properties.containsKey("nocturne.sticky-template-paths")) {
+ String stickyTemplatePaths = StringUtils.trimToEmpty(properties.getProperty("nocturne.sticky-template-paths"));
+ if (!stickyTemplatePaths.isEmpty()) {
+ ApplicationContext.getInstance().setStickyTemplatePaths(Boolean.parseBoolean(stickyTemplatePaths));
+ }
+ }
+
+ if (properties.containsKey("nocturne.use-component-templates")) {
+ String useComponentTemplates = properties.getProperty("nocturne.use-component-templates");
+ if (!"false".equals(useComponentTemplates) && !"true".equals(useComponentTemplates)) {
+ logger.error("Parameter nocturne.use-component-templates expected to be 'false' or 'true'.");
+ throw new ConfigurationException("Parameter nocturne.use-component-templates expected to be 'false' or 'true'.");
+ }
+ boolean use = "true".equals(useComponentTemplates);
+ ApplicationContext.getInstance().setUseComponentTemplates(use);
+ if (use && properties.containsKey("nocturne.component-templates-less-commons-file")) {
+ String componentTemplatesLessCommonsFileAsString
+ = properties.getProperty("nocturne.component-templates-less-commons-file");
+ if (!StringUtil.isBlank(componentTemplatesLessCommonsFileAsString)) {
+ File componentTemplatesLessCommonsFile = new File(componentTemplatesLessCommonsFileAsString);
+ if (componentTemplatesLessCommonsFile.isFile()) {
+ ApplicationContext.getInstance().setComponentTemplatesLessCommonsFile(componentTemplatesLessCommonsFile);
+ } else {
+ logger.error("Parameter nocturne.component-templates-less-commons-file is expected to be a file.");
+ throw new ConfigurationException("Parameter nocturne.component-templates-less-commons-file is expected to be a file.");
+ }
+ }
+ }
+ }
+ }
+
+ private static void setupDebug() {
+ ApplicationContext.getInstance().setDebug(Boolean.parseBoolean(properties.getProperty("nocturne.debug")));
+ }
+
+ private static List listOfNonEmpties(String[] strings) {
+ List result = new ArrayList<>(strings.length);
+ for (String s : strings) {
+ if (!StringUtil.isEmpty(s)) {
+ result.add(s);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Scans classpath for modules.
+ *
+ * @return List of modules ordered by priority (from high priority to low).
+ */
+ private static List getModulesFromClasspath() {
+ List modules = new ArrayList<>();
+ URLClassLoader loader = (URLClassLoader) ApplicationContext.class.getClassLoader();
+ URL[] classPath = loader.getURLs();
+ for (URL url : classPath) {
+ if (Module.isModuleUrl(url)) {
+ modules.add(new Module(url));
+ }
+ }
+ return modules;
+ }
+
+ /**
+ * Runs init() method for all modules.
+ * Each module should be initialized on the application startup.
+ */
+ private static void initializeModules() {
+ List modules = getModulesFromClasspath();
+
+ for (Module module : modules) {
+ module.init();
+ }
+
+ modules.sort((moduleA, moduleB) -> {
+ int priorityComparisonResult = Integer.compare(moduleB.getPriority(), moduleA.getPriority());
+ if (priorityComparisonResult != 0) {
+ return priorityComparisonResult;
+ }
+
+ return moduleA.getName().compareTo(moduleB.getName());
+ });
+
+ for (Module module : modules) {
+ module.getConfiguration().addPages();
+ }
+
+ ApplicationContext.getInstance().setModules(modules);
+ }
+
+ private static void setupInjector() {
+ String guiceModuleClassName = ApplicationContext.getInstance().getGuiceModuleClassName();
+ GenericIocModule module = new GenericIocModule();
+
+ if (!StringUtil.isEmpty(guiceModuleClassName)) {
+ try {
+ module.setModule(getApplicationModule(guiceModuleClassName));
+ } catch (Exception e) {
+ logger.error("Can't load application Guice module.", e);
+ throw new ConfigurationException("Can't load application Guice module.", e);
+ }
+ }
+
+ Injector injector = Guice.createInjector(module);
+
+ if (ApplicationContext.getInstance().isDebug()) {
+ try {
+ Method method = ApplicationContext.class.getDeclaredMethod("setInjector", Injector.class);
+ method.setAccessible(true);
+ method.invoke(ApplicationContext.getInstance(), injector);
+ } catch (NoSuchMethodException e) {
+ logger.error("Can't find method setInjector.", e);
+ throw new NocturneException("Can't find method setInjector.", e);
+ } catch (InvocationTargetException e) {
+ logger.error("InvocationTargetException", e);
+ throw new NocturneException("InvocationTargetException", e);
+ } catch (IllegalAccessException e) {
+ logger.error("IllegalAccessException", e);
+ throw new NocturneException("IllegalAccessException", e);
+ }
+ } else {
+ ApplicationContext.getInstance().setInjector(injector);
+ }
+ }
+
+ private static com.google.inject.Module getApplicationModule(String guiceModuleClassName) throws Exception {
+ Class> moduleClass = ApplicationContext.class.getClassLoader().loadClass(guiceModuleClassName);
+ AtomicReference exception = new AtomicReference<>();
+
+ try {
+ return (com.google.inject.Module) moduleClass.getConstructor().newInstance();
+ } catch (Exception e) {
+ exception.compareAndSet(null, e);
+ }
+
+ try {
+ Method getInstanceMethod = moduleClass.getMethod("getInstance");
+ if (Modifier.isStatic(getInstanceMethod.getModifiers())
+ && com.google.inject.Module.class.isAssignableFrom(getInstanceMethod.getReturnType())) {
+ return (com.google.inject.Module) getInstanceMethod.invoke(null);
+ }
+ } catch (Exception e) {
+ exception.compareAndSet(null, e);
+ }
+
+ try {
+ Method createInstanceMethod = moduleClass.getMethod("createInstance");
+ if (Modifier.isStatic(createInstanceMethod.getModifiers())
+ && com.google.inject.Module.class.isAssignableFrom(createInstanceMethod.getReturnType())) {
+ return (com.google.inject.Module) createInstanceMethod.invoke(null);
+ }
+ } catch (Exception e) {
+ exception.compareAndSet(null, e);
+ }
+
+ try {
+ Method newInstanceMethod = moduleClass.getMethod("newInstance");
+ if (Modifier.isStatic(newInstanceMethod.getModifiers())
+ && com.google.inject.Module.class.isAssignableFrom(newInstanceMethod.getReturnType())) {
+ return (com.google.inject.Module) newInstanceMethod.invoke(null);
+ }
+ } catch (Exception e) {
+ exception.compareAndSet(null, e);
+ }
+
+ throw exception.get();
+ }
+
+ private static void runModuleStartups() {
+ List modules = ApplicationContext.getInstance().getModules();
+ for (Module module : modules) {
+ String startupClassName = module.getStartupClassName();
+ if (!startupClassName.isEmpty()) {
+ Runnable runnable;
+ try {
+ runnable = (Runnable) ApplicationContext.getInstance().getInjector().getInstance(
+ ApplicationContext.class.getClassLoader().loadClass(startupClassName));
+ } catch (ClassCastException e) {
+ logger.error("Startup class " + startupClassName + " must implement Runnable.", e);
+ throw new ModuleInitializationException("Startup class " + startupClassName
+ + " must implement Runnable.", e);
+ } catch (ClassNotFoundException e) {
+ logger.error("Can't load startup class be name " + startupClassName + '.', e);
+ throw new ModuleInitializationException("Can't load startup class be name "
+ + startupClassName + '.', e);
+ }
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+ }
+ }
+
+ static void initialize() {
+ synchronized (ApplicationContextLoader.class) {
+ run();
+ initializeModules();
+ setupInjector();
+ runModuleStartups();
+ ApplicationContext.getInstance().setInitialized();
+ }
+ }
+
+ static void shutdown() {
+ synchronized (ApplicationContextLoader.class) {
+ ApplicationContext.getInstance().getModules().parallelStream()
+ .map(Module::getConfiguration).filter(Objects::nonNull).forEach(Configuration::shutdown);
+ }
+ }
+
+ static {
+ try (InputStream inputStream = ApplicationContextLoader.class.getResourceAsStream(Constants.CONFIGURATION_FILE)) {
+ properties.load(inputStream);
+ } catch (IOException e) {
+ logger.error("Can't load resource file " + Constants.CONFIGURATION_FILE + '.', e);
+ throw new ConfigurationException("Can't load resource file " + Constants.CONFIGURATION_FILE + '.', e);
+ }
+ }
+}
diff --git a/code/src/main/java/org/nocturne/main/ApplicationTemplateLoader.java b/code/src/main/java/org/nocturne/main/ApplicationTemplateLoader.java
index 2d2b735..c884678 100644
--- a/code/src/main/java/org/nocturne/main/ApplicationTemplateLoader.java
+++ b/code/src/main/java/org/nocturne/main/ApplicationTemplateLoader.java
@@ -1,132 +1,132 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import freemarker.cache.TemplateLoader;
-import org.apache.log4j.Logger;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.module.Module;
-import org.nocturne.module.PreprocessFreemarkerFileTemplateLoader;
-import org.nocturne.util.FileUtil;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.Reader;
-import java.util.List;
-import java.util.Map;
-import java.util.WeakHashMap;
-
-/**
- * This template loader will delegate all the requests to
- * standard FileTemplateLoader in production mode or
- * loads template from modules DebugContext in debug mode.
- */
-public class ApplicationTemplateLoader implements TemplateLoader {
- /**
- * Logger.
- */
- private static final Logger logger = Logger.getLogger(ApplicationTemplateLoader.class);
-
- /**
- * List of loaded modules.
- */
- private final List modules;
-
- /**
- * Instance of ApplicationContext - just shortcut.
- */
- private final ApplicationContext applicationContext = ApplicationContext.getInstance();
-
- /**
- * For debug mode stores loader by loaded object.
- */
- private final Map loadersByTemplate = new WeakHashMap<>();
-
- /**
- * Usual file template loader, uses nocturne.templates-path.
- */
- private final TemplateLoader templateLoader;
-
- /**
- * New ApplicationTemplateLoader.
- */
- public ApplicationTemplateLoader() {
- modules = applicationContext.getModules();
-
- String[] templatePaths = applicationContext.getTemplatePaths();
- int templateDirCount = templatePaths.length;
- File[] templateDirs = new File[templateDirCount];
-
- for (int dirIndex = 0; dirIndex < templateDirCount; ++dirIndex) {
- String templatePath = templatePaths[dirIndex];
- File templatePathFile = new File(templatePath);
-
- if (!templatePathFile.isAbsolute() || !templatePathFile.exists()) {
- String realTemplatePath = FileUtil.getRealPath(applicationContext.getServletContext(), templatePath);
- if (realTemplatePath == null) {
- throw new NocturneException("Can't find '" + templatePath + "' in servletContext.");
- } else {
- templatePath = realTemplatePath;
- }
- }
-
- templatePathFile = new File(templatePath);
- if (!templatePathFile.exists()) {
- throw new NocturneException("Can't find template path '" + templatePath + "' in servletContext.");
- }
-
- templateDirs[dirIndex] = templatePathFile;
- }
-
- try {
- templateLoader = new PreprocessFreemarkerFileTemplateLoader(templateDirs);
- } catch (IOException e) {
- throw new NocturneException("Can't create FileTemplateLoader for delegation.", e);
- }
- }
-
- @Override
- public Object findTemplateSource(String s) throws IOException {
- if (applicationContext.isDebug()) {
- for (Module module : modules) {
- Object result = module.getTemplateLoader().findTemplateSource(s);
- if (result != null) {
- loadersByTemplate.put(result, module.getTemplateLoader());
- return result;
- }
- }
- }
- return templateLoader.findTemplateSource(s);
- }
-
- @Override
- public long getLastModified(Object o) {
- if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
- return loadersByTemplate.get(o).getLastModified(o);
- }
- return templateLoader.getLastModified(o);
- }
-
- @Override
- public Reader getReader(Object o, String s) throws IOException {
- if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
- return loadersByTemplate.get(o).getReader(o, s);
- }
-
- return templateLoader.getReader(o, s);
- }
-
- @Override
- public void closeTemplateSource(Object o) throws IOException {
- if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
- TemplateLoader loader = loadersByTemplate.get(o);
- if (loader != null) {
- loader.closeTemplateSource(o);
- loadersByTemplate.remove(o);
- }
- }
-
- templateLoader.closeTemplateSource(o);
- }
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.main;
+
+import freemarker.cache.TemplateLoader;
+import org.apache.log4j.Logger;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.module.Module;
+import org.nocturne.module.PreprocessFreemarkerFileTemplateLoader;
+import org.nocturne.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * This template loader will delegate all the requests to
+ * standard FileTemplateLoader in production mode or
+ * loads template from modules DebugContext in debug mode.
+ */
+public class ApplicationTemplateLoader implements TemplateLoader {
+ /**
+ * Logger.
+ */
+ private static final Logger logger = Logger.getLogger(ApplicationTemplateLoader.class);
+
+ /**
+ * List of loaded modules.
+ */
+ private final List modules;
+
+ /**
+ * Instance of ApplicationContext - just shortcut.
+ */
+ private final ApplicationContext applicationContext = ApplicationContext.getInstance();
+
+ /**
+ * For debug mode stores loader by loaded object.
+ */
+ private final Map loadersByTemplate = new WeakHashMap<>();
+
+ /**
+ * Usual file template loader, uses nocturne.templates-path.
+ */
+ private final TemplateLoader templateLoader;
+
+ /**
+ * New ApplicationTemplateLoader.
+ */
+ public ApplicationTemplateLoader() {
+ modules = applicationContext.getModules();
+
+ String[] templatePaths = applicationContext.getTemplatePaths();
+ int templateDirCount = templatePaths.length;
+ File[] templateDirs = new File[templateDirCount];
+
+ for (int dirIndex = 0; dirIndex < templateDirCount; ++dirIndex) {
+ String templatePath = templatePaths[dirIndex];
+ File templatePathFile = new File(templatePath);
+
+ if (!templatePathFile.isAbsolute() || !templatePathFile.exists()) {
+ String realTemplatePath = FileUtil.getRealPath(applicationContext.getServletContext(), templatePath);
+ if (realTemplatePath == null) {
+ throw new NocturneException("Can't find '" + templatePath + "' in servletContext.");
+ } else {
+ templatePath = realTemplatePath;
+ }
+ }
+
+ templatePathFile = new File(templatePath);
+ if (!templatePathFile.exists()) {
+ throw new NocturneException("Can't find template path '" + templatePath + "' in servletContext.");
+ }
+
+ templateDirs[dirIndex] = templatePathFile;
+ }
+
+ try {
+ templateLoader = new PreprocessFreemarkerFileTemplateLoader(templateDirs);
+ } catch (IOException e) {
+ throw new NocturneException("Can't create FileTemplateLoader for delegation.", e);
+ }
+ }
+
+ @Override
+ public Object findTemplateSource(String s) throws IOException {
+ if (applicationContext.isDebug()) {
+ for (Module module : modules) {
+ Object result = module.getTemplateLoader().findTemplateSource(s);
+ if (result != null) {
+ loadersByTemplate.put(result, module.getTemplateLoader());
+ return result;
+ }
+ }
+ }
+ return templateLoader.findTemplateSource(s);
+ }
+
+ @Override
+ public long getLastModified(Object o) {
+ if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
+ return loadersByTemplate.get(o).getLastModified(o);
+ }
+ return templateLoader.getLastModified(o);
+ }
+
+ @Override
+ public Reader getReader(Object o, String s) throws IOException {
+ if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
+ return loadersByTemplate.get(o).getReader(o, s);
+ }
+
+ return templateLoader.getReader(o, s);
+ }
+
+ @Override
+ public void closeTemplateSource(Object o) throws IOException {
+ if (applicationContext.isDebug() && loadersByTemplate.containsKey(o)) {
+ TemplateLoader loader = loadersByTemplate.get(o);
+ if (loader != null) {
+ loader.closeTemplateSource(o);
+ loadersByTemplate.remove(o);
+ }
+ }
+
+ templateLoader.closeTemplateSource(o);
+ }
+}
diff --git a/code/src/main/java/org/nocturne/main/Constants.java b/code/src/main/java/org/nocturne/main/Constants.java
index 81e8ab9..edc20c7 100644
--- a/code/src/main/java/org/nocturne/main/Constants.java
+++ b/code/src/main/java/org/nocturne/main/Constants.java
@@ -1,18 +1,18 @@
-package org.nocturne.main;
-
-import freemarker.template.Configuration;
-import freemarker.template.Version;
-
-/**
- * @author Maxim Shipko (sladethe@gmail.com)
- * Date: 10.02.15
- */
-@SuppressWarnings("WeakerAccess")
-public class Constants {
- public static final String CONFIGURATION_FILE = "/nocturne.properties";
- public static final Version FREEMARKER_VERSION = Configuration.VERSION_2_3_21;
-
- private Constants() {
- throw new UnsupportedOperationException();
- }
-}
+package org.nocturne.main;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * @author Maxim Shipko (sladethe@gmail.com)
+ * Date: 10.02.15
+ */
+@SuppressWarnings("WeakerAccess")
+public class Constants {
+ public static final String CONFIGURATION_FILE = "/nocturne.properties";
+ public static final Version FREEMARKER_VERSION = Configuration.VERSION_2_3_21;
+
+ private Constants() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/code/src/main/java/org/nocturne/main/DebugResourceFilter.java b/code/src/main/java/org/nocturne/main/DebugResourceFilter.java
index 5456945..9acf86c 100644
--- a/code/src/main/java/org/nocturne/main/DebugResourceFilter.java
+++ b/code/src/main/java/org/nocturne/main/DebugResourceFilter.java
@@ -1,215 +1,215 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import eu.medsea.mimeutil.MimeType;
-import eu.medsea.mimeutil.detector.ExtensionMimeDetector;
-import org.apache.log4j.Logger;
-import org.jetbrains.annotations.Contract;
-import org.nocturne.exception.NocturneException;
-import org.nocturne.exception.ReflectionException;
-import org.nocturne.module.Module;
-import org.nocturne.util.ReflectionUtil;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.*;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * You may use this filter only for debug purpose.
- * Use it for css, js and image resources. It loads
- * resources from modules and if you change
- * resources in IDE it will load renewed version.
- */
-@SuppressWarnings({"unused"})
-public class DebugResourceFilter implements Filter {
- private static final Logger logger = Logger.getLogger(DebugResourceFilter.class);
-
- static {
- String mimeDetectorName = ExtensionMimeDetector.class.getName();
- if (eu.medsea.mimeutil.MimeUtil.getMimeDetector(mimeDetectorName) == null) {
- eu.medsea.mimeutil.MimeUtil.registerMimeDetector(mimeDetectorName);
- }
- }
-
- @SuppressWarnings("RedundantThrows")
- @Override
- public void init(FilterConfig filterConfig) throws ServletException {
- // No operations.
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
- if (ReloadingContext.getInstance().isDebug()) {
- DispatchFilter.updateRequestDispatcher();
-
- if (getClass().getClassLoader() == DispatchFilter.lastReloadingClassLoader) {
- handleDebugModeDoFilter(request, response, chain);
- } else {
- Object object;
-
- try {
- object = DispatchFilter.lastReloadingClassLoader.loadClass(DebugResourceFilter.class.getName()).getConstructor().newInstance();
- } catch (Exception e) {
- logger.error("Can't create instance of DebugResourceFilter.", e);
- throw new NocturneException("Can't create instance of DebugResourceFilter.", e);
- }
-
- try {
- ReflectionUtil.invoke(object, "handleDebugModeDoFilter", request, response, chain);
- } catch (ReflectionException e) {
- logger.error("Can't run DebugResourceFilter.", e);
- throw new NocturneException("Can't run DebugResourceFilter.", e);
- }
- }
- } else {
- chain.doFilter(request, response);
- }
- }
-
- private static void handleDebugModeDoFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
- if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- String path = httpRequest.getServletPath();
-
- List modules = ApplicationContext.getInstance().getModules();
- for (Module module : modules) {
- if (processModuleResource(module, path, response)) {
- return;
- }
- }
-
- String resourcesDir = ApplicationContext.getInstance().getDebugWebResourcesDir();
- if (resourcesDir != null) {
- File resourceFile = new File(resourcesDir, path);
- if (resourceFile.isFile()) {
- InputStream resourceInputStream = new FileInputStream(resourceFile);
- writeResourceByPathAndStream(response, path, resourceInputStream);
- return;
- }
- }
-
- filterChain.doFilter(request, response);
- }
- }
-
- private static boolean processModuleResource(Module module, String path, ServletResponse response) throws IOException {
- InputStream inputStream = module.getResourceLoader().getResourceInputStream(path);
- return writeResourceByPathAndStream(response, path, inputStream);
- }
-
- private static boolean writeResourceByPathAndStream(ServletResponse response, String path, InputStream inputStream) throws IOException {
- if (inputStream != null) {
- try (OutputStream outputStream = response.getOutputStream()) {
- setupContentType(path, response);
-
- int size = 0;
- byte[] buffer = new byte[65536];
-
- while (true) {
- int readCount = inputStream.read(buffer);
-
- if (readCount >= 0) {
- outputStream.write(buffer, 0, readCount);
- size += readCount;
- } else {
- break;
- }
- }
- response.setContentLength(size);
- } finally {
- inputStream.close();
- }
-
- return true;
- } else {
- return false;
- }
- }
-
- @Contract("null, _ -> fail")
- private static void setupContentType(String path, ServletResponse response) {
- if (path != null) {
- String mimeType = MimeUtil.getMimeType(path);
-
- if (mimeType != null) {
- response.setContentType(mimeType);
- return;
- }
- }
-
- throw new org.nocturne.exception.ServletException("Can't set content type for " + path + '.');
- }
-
- @Override
- public void destroy() {
- }
-
- private static final class MimeUtil {
- private static final Map mimeTypeByExtension = new ConcurrentHashMap<>();
-
- private static void add(String mimeType, String... extensions) {
- for (String extension : extensions) {
- if (mimeTypeByExtension.containsKey(extension)) {
- throw new NocturneException("Already has registered mime type by " + extension + '.');
- }
- mimeTypeByExtension.put(extension, mimeType);
- }
- }
-
- private static String getMimeType(String path) {
- String extension = (path.indexOf('.') < 0 ? path : path.substring(path.lastIndexOf('.') + 1)).toLowerCase();
- String result = mimeTypeByExtension.get(extension);
- if (result != null) {
- return result;
- }
-
- MimeType mimeType = eu.medsea.mimeutil.MimeUtil.getMostSpecificMimeType(eu.medsea.mimeutil.MimeUtil.getMimeTypes(new File(path).getName()));
- if (mimeType != null) {
- return mimeType.toString();
- }
-
- return "application/octet-stream";
- }
-
- static {
- add("application/wasm", "wasm");
- add("application/json", "json");
- add("application/javascript", "js");
- add("application/pdf", "pdf");
- add("application/postscript", "ps");
- add("application/font-woff", "woff");
- add("application/xhtml+xml", "xhtml");
- add("application/xml-dtd", "dtd");
- add("application/zip", "zip");
- add("application/gzip", "gzip");
- add("application/x-tex", "tex");
- add("application/xml", "xml");
- add("audio/aac", "acc");
- add("audio/mpeg", "mp3");
- add("audio/ogg", "ogg");
- add("image/gif", "gif");
- add("image/jpeg", "jpeg");
- add("image/png", "png");
- add("image/svg+xml", "svg");
- add("image/tiff", "tiff");
- add("image/webp", "webp");
- add("image/bmp", "bmp");
- add("text/plain", "txt");
- add("text/css", "css");
- add("text/html", "html", "htm");
- add("text/x-java-source", "java");
- add("text/x-c", "cpp");
- add("text/x-c", "c");
- add("video/avi", "avi");
- add("video/mp4", "mp4");
- add("video/mpeg", "mpeg");
- }
- }
-
-}
+/*
+ * Copyright 2009 Mike Mirzayanov
+ */
+package org.nocturne.main;
+
+import eu.medsea.mimeutil.MimeType;
+import eu.medsea.mimeutil.detector.ExtensionMimeDetector;
+import org.apache.log4j.Logger;
+import org.jetbrains.annotations.Contract;
+import org.nocturne.exception.NocturneException;
+import org.nocturne.exception.ReflectionException;
+import org.nocturne.module.Module;
+import org.nocturne.util.ReflectionUtil;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * You may use this filter only for debug purpose.
+ * Use it for css, js and image resources. It loads
+ * resources from modules and if you change
+ * resources in IDE it will load renewed version.
+ */
+@SuppressWarnings({"unused"})
+public class DebugResourceFilter implements Filter {
+ private static final Logger logger = Logger.getLogger(DebugResourceFilter.class);
+
+ static {
+ String mimeDetectorName = ExtensionMimeDetector.class.getName();
+ if (eu.medsea.mimeutil.MimeUtil.getMimeDetector(mimeDetectorName) == null) {
+ eu.medsea.mimeutil.MimeUtil.registerMimeDetector(mimeDetectorName);
+ }
+ }
+
+ @SuppressWarnings("RedundantThrows")
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // No operations.
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (ReloadingContext.getInstance().isDebug()) {
+ DispatchFilter.updateRequestDispatcher();
+
+ if (getClass().getClassLoader() == DispatchFilter.lastReloadingClassLoader) {
+ handleDebugModeDoFilter(request, response, chain);
+ } else {
+ Object object;
+
+ try {
+ object = DispatchFilter.lastReloadingClassLoader.loadClass(DebugResourceFilter.class.getName()).getConstructor().newInstance();
+ } catch (Exception e) {
+ logger.error("Can't create instance of DebugResourceFilter.", e);
+ throw new NocturneException("Can't create instance of DebugResourceFilter.", e);
+ }
+
+ try {
+ ReflectionUtil.invoke(object, "handleDebugModeDoFilter", request, response, chain);
+ } catch (ReflectionException e) {
+ logger.error("Can't run DebugResourceFilter.", e);
+ throw new NocturneException("Can't run DebugResourceFilter.", e);
+ }
+ }
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ private static void handleDebugModeDoFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
+ if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ String path = httpRequest.getServletPath();
+
+ List modules = ApplicationContext.getInstance().getModules();
+ for (Module module : modules) {
+ if (processModuleResource(module, path, response)) {
+ return;
+ }
+ }
+
+ String resourcesDir = ApplicationContext.getInstance().getDebugWebResourcesDir();
+ if (resourcesDir != null) {
+ File resourceFile = new File(resourcesDir, path);
+ if (resourceFile.isFile()) {
+ InputStream resourceInputStream = new FileInputStream(resourceFile);
+ writeResourceByPathAndStream(response, path, resourceInputStream);
+ return;
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+ }
+
+ private static boolean processModuleResource(Module module, String path, ServletResponse response) throws IOException {
+ InputStream inputStream = module.getResourceLoader().getResourceInputStream(path);
+ return writeResourceByPathAndStream(response, path, inputStream);
+ }
+
+ private static boolean writeResourceByPathAndStream(ServletResponse response, String path, InputStream inputStream) throws IOException {
+ if (inputStream != null) {
+ try (OutputStream outputStream = response.getOutputStream()) {
+ setupContentType(path, response);
+
+ int size = 0;
+ byte[] buffer = new byte[65536];
+
+ while (true) {
+ int readCount = inputStream.read(buffer);
+
+ if (readCount >= 0) {
+ outputStream.write(buffer, 0, readCount);
+ size += readCount;
+ } else {
+ break;
+ }
+ }
+ response.setContentLength(size);
+ } finally {
+ inputStream.close();
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Contract("null, _ -> fail")
+ private static void setupContentType(String path, ServletResponse response) {
+ if (path != null) {
+ String mimeType = MimeUtil.getMimeType(path);
+
+ if (mimeType != null) {
+ response.setContentType(mimeType);
+ return;
+ }
+ }
+
+ throw new org.nocturne.exception.ServletException("Can't set content type for " + path + '.');
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ private static final class MimeUtil {
+ private static final Map mimeTypeByExtension = new ConcurrentHashMap<>();
+
+ private static void add(String mimeType, String... extensions) {
+ for (String extension : extensions) {
+ if (mimeTypeByExtension.containsKey(extension)) {
+ throw new NocturneException("Already has registered mime type by " + extension + '.');
+ }
+ mimeTypeByExtension.put(extension, mimeType);
+ }
+ }
+
+ private static String getMimeType(String path) {
+ String extension = (path.indexOf('.') < 0 ? path : path.substring(path.lastIndexOf('.') + 1)).toLowerCase();
+ String result = mimeTypeByExtension.get(extension);
+ if (result != null) {
+ return result;
+ }
+
+ MimeType mimeType = eu.medsea.mimeutil.MimeUtil.getMostSpecificMimeType(eu.medsea.mimeutil.MimeUtil.getMimeTypes(new File(path).getName()));
+ if (mimeType != null) {
+ return mimeType.toString();
+ }
+
+ return "application/octet-stream";
+ }
+
+ static {
+ add("application/wasm", "wasm");
+ add("application/json", "json");
+ add("application/javascript", "js");
+ add("application/pdf", "pdf");
+ add("application/postscript", "ps");
+ add("application/font-woff", "woff");
+ add("application/xhtml+xml", "xhtml");
+ add("application/xml-dtd", "dtd");
+ add("application/zip", "zip");
+ add("application/gzip", "gzip");
+ add("application/x-tex", "tex");
+ add("application/xml", "xml");
+ add("audio/aac", "acc");
+ add("audio/mpeg", "mp3");
+ add("audio/ogg", "ogg");
+ add("image/gif", "gif");
+ add("image/jpeg", "jpeg");
+ add("image/png", "png");
+ add("image/svg+xml", "svg");
+ add("image/tiff", "tiff");
+ add("image/webp", "webp");
+ add("image/bmp", "bmp");
+ add("text/plain", "txt");
+ add("text/css", "css");
+ add("text/html", "html", "htm");
+ add("text/x-java-source", "java");
+ add("text/x-c", "cpp");
+ add("text/x-c", "c");
+ add("video/avi", "avi");
+ add("video/mp4", "mp4");
+ add("video/mpeg", "mpeg");
+ }
+ }
+
+}
diff --git a/code/src/main/java/org/nocturne/main/DispatchFilter.java b/code/src/main/java/org/nocturne/main/DispatchFilter.java
index 93011e5..3a16ff4 100644
--- a/code/src/main/java/org/nocturne/main/DispatchFilter.java
+++ b/code/src/main/java/org/nocturne/main/DispatchFilter.java
@@ -1,223 +1,223 @@
-/*
- * Copyright 2009 Mike Mirzayanov
- */
-package org.nocturne.main;
-
-import org.nocturne.exception.NocturneException;
-import org.nocturne.exception.ReflectionException;
-import org.nocturne.util.FileUtil;
-import org.nocturne.util.ReflectionUtil;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- *
- * Main nocturne filter to dispatch requests.
- *
- *
- * In will create new ReloadingClassLoader on each request
- * (if changes found and more than 500 ms passed since last request) in the debug mode.
- * This class loader will load updated classes of your application.
- *
- *
- * In the production mode it uses usual webapp class loader.
- *
+ * Main nocturne filter to dispatch requests.
+ *
+ *
+ * In will create new ReloadingClassLoader on each request
+ * (if changes found and more than 500 ms passed since last request) in the debug mode.
+ * This class loader will load updated classes of your application.
+ *
+ *
+ * In the production mode it uses usual webapp class loader.
+ *
- * Use it to listen events and fire them. Any object can be event. Listeners
- * are subscribed to class. When executed {@code fire(event)} all listeners for class
- * event.getClass() will be notified. Also all listeners
- * for event.getClass().getSuperclass() (and so on) will be notified.
- *
- *
- * Use pair of methods beforeAction() and afterAction() to listen components.
- * Any component will notify all listeners registered with beforeAction()
- * before process action and will notify all listeners registered with afterAction()
- * after process action.
- *
- *
- * @author Mike Mirzayanov
- */
-@SuppressWarnings("unused")
-public class Events {
- /**
- * Each class has no more than MAX_LISTENER_COUNT listeners.
- * If you are trying to add more, an exception will be thrown.
- * Usually it means that you are trying to add listeners on each request,
- * but you shouldn't do it
- */
- private static final int MAX_LISTENER_COUNT = 20;
-
- private static final Scope COMMON_SCOPE = new Scope();
- private static final Scope BEFORE_ACTION_SCOPE = new Scope();
- private static final Scope AFTER_ACTION_SCOPE = new Scope();
-
- /**
- * Add listener to events of class "eventClass".
- *
- * @param Event class.
- * @param eventClass Class to be listened. If event has "eventClass" as its
- * superclass listeners will be notified too.
- * @param listener Listener instance.
- */
- public static void listen(Class eventClass, Listener listener) {
- COMMON_SCOPE.listen(eventClass, listener);
- }
-
- /**
- * @param Event class.
- * @param event Throwing event. All listeners registered for class event.getClass()
- * or its superclass will be notified.
- * @return Fired event.
- */
- public static T fire(T event) {
- return COMMON_SCOPE.fire(event);
- }
-
- /**
- * @param Component class.
- * @param componentClass Component class to be listened.
- * @param listener Listener which will be notified before any action
- * for componentClass will be processed.
- */
- public static void beforeAction(Class componentClass, Listener listener) {
- BEFORE_ACTION_SCOPE.listen(componentClass, listener);
- }
-
- static void fireBeforeAction(Component component) {
- BEFORE_ACTION_SCOPE.fire(component);
- }
-
- /**
- * @param Component class.
- * @param componentClass Component class to be listened.
- * @param listener Listener which will be notified after any action
- * for componentClass will be processed.
- */
- public static void afterAction(Class componentClass, Listener listener) {
- AFTER_ACTION_SCOPE.listen(componentClass, listener);
- }
-
- static void fireAfterAction(Component component) {
- AFTER_ACTION_SCOPE.fire(component);
- }
-
- @SuppressWarnings("WeakerAccess")
- private static class Scope {
- /**
- * Stores listeners for each .
- */
- private final Map, Set