package org.ff4j.web.embedded;
import static org.ff4j.web.embedded.ConsoleConstants.CONTENT_TYPE_CSS;
import static org.ff4j.web.embedded.ConsoleConstants.CONTENT_TYPE_HTML;
import static org.ff4j.web.embedded.ConsoleConstants.CONTENT_TYPE_JS;
import static org.ff4j.web.embedded.ConsoleConstants.FEATID;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_ALERT_MESSAGE;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_AUDIT_ROWS;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_FEATURE_ROWS;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_GROUP_LIST_CREATE;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_GROUP_LIST_EDIT;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_GROUP_LIST_TOGGLE;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_PERMISSIONLIST;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_PROPERTIES_ROWS;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_SERVLET_CONTEXT;
import static org.ff4j.web.embedded.ConsoleConstants.KEY_VERSION;
import static org.ff4j.web.embedded.ConsoleConstants.MODAL_CREATE;
import static org.ff4j.web.embedded.ConsoleConstants.MODAL_EDIT;
import static org.ff4j.web.embedded.ConsoleConstants.MODAL_TOGGLE;
import static org.ff4j.web.embedded.ConsoleConstants.NEW_LINE;
import static org.ff4j.web.embedded.ConsoleConstants.OP_RMV_FEATURE;
import static org.ff4j.web.embedded.ConsoleConstants.OP_RMV_PROPERTY;
import static org.ff4j.web.embedded.ConsoleConstants.PREFIX_CHECKBOX;
import static org.ff4j.web.embedded.ConsoleConstants.RESOURCE;
import static org.ff4j.web.embedded.ConsoleConstants.RESOURCE_CSS_FILE;
import static org.ff4j.web.embedded.ConsoleConstants.RESOURCE_CSS_PARAM;
import static org.ff4j.web.embedded.ConsoleConstants.RESOURCE_JS_FILE;
import static org.ff4j.web.embedded.ConsoleConstants.RESOURCE_JS_PARAM;
import static org.ff4j.web.embedded.ConsoleConstants.TEMPLATE_FILE;
import static org.ff4j.web.embedded.ConsoleConstants.TEMPLATE_FILE_MONITORING;
import static org.ff4j.web.embedded.ConsoleConstants.UTF8_ENCODING;
import java.io.IOException;
/*
* #%L AdministrationConsoleRenderer.java (ff4j-web) by Cedrick LUNVEN %% Copyright (C) 2013 Ff4J %% Licensed under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License. #L%
*/
import java.io.InputStream;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.ff4j.FF4j;
import org.ff4j.audit.Event;
import org.ff4j.audit.EventQueryDefinition;
import org.ff4j.audit.repository.EventRepository;
import org.ff4j.core.Feature;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.property.Property;
import org.ff4j.property.PropertyBigDecimal;
import org.ff4j.property.PropertyBigInteger;
import org.ff4j.property.PropertyBoolean;
import org.ff4j.property.PropertyByte;
import org.ff4j.property.PropertyDouble;
import org.ff4j.property.PropertyFloat;
import org.ff4j.property.PropertyInt;
import org.ff4j.property.PropertyLogLevel;
import org.ff4j.property.PropertyLong;
import org.ff4j.property.PropertyShort;
import org.ff4j.property.PropertyString;
import org.ff4j.utils.Util;
/**
* Used to build GUI Interface for feature flip servlet. It contains gui component render and parmeters
*
* @author <a href="mailto:cedrick.lunven@gmail.com">Cedrick LUNVEN</a>
*/
public final class ConsoleRenderer {
/** Cache for page blocks. */
private static String htmlTemplate = null;
/** Cache for page blocks. */
private static String htmlTemplateMonitoring = null;
/** Load CSS. */
private static String cssContent = null;
/** Load JS. */
private static String jsContent = null;
/** Cache for page blocks. */
static final String TABLE_FEATURES_FOOTER = "" + "</tbody></table></form></fieldset>";
/** fin de ligne. **/
static final String END_OF_LINE = "\r\n";
/** Get version of the component. */
static final String FF4J_VERSION = ConsoleRenderer.class.getPackage().getImplementationVersion();
/** Display audit log date. */
static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-dd-MM HH:mm:ss");
/** Mapping from simple 'String' <=> 'org.ff4j.property.PropertyString'. */
private static Map < String , String > uxTypes = new HashMap< String , String>();
/**
* Initialized Primitive to work with Properties.
*/
static {
uxTypes.put(Byte.class.getSimpleName(), PropertyByte.class.getName());
uxTypes.put(Short.class.getSimpleName(), PropertyShort.class.getName());
uxTypes.put(Integer.class.getSimpleName(), PropertyInt.class.getName());
uxTypes.put(Long.class.getSimpleName(), PropertyLong.class.getName());
uxTypes.put(Double.class.getSimpleName(), PropertyDouble.class.getName());
uxTypes.put(Boolean.class.getSimpleName(), PropertyBoolean.class.getName());
uxTypes.put(Float.class.getSimpleName(), PropertyFloat.class.getName());
uxTypes.put(BigInteger.class.getSimpleName(), PropertyBigInteger.class.getName());
uxTypes.put(BigDecimal.class.getSimpleName(), PropertyBigDecimal.class.getName());
uxTypes.put("LogLevel", PropertyLogLevel.class.getName());
uxTypes.put(String.class.getSimpleName(), PropertyString.class.getName());
}
private ConsoleRenderer() {}
/**
* Render the ff4f console webpage through different block.
*
* @param req
* http request (with parameters)
* @param res
* http response (with outouput test)
* @param message
* text in the information box (blue/green/orange/red)
* @param messagetype
* type of informatice message (info,success,warning,error)
* @throws IOException
* error during populating http response
*/
public static void renderPage(FF4j ff4j, HttpServletRequest req, HttpServletResponse res, String msg, String msgType) throws IOException {
res.setContentType(CONTENT_TYPE_HTML);
PrintWriter out = res.getWriter();
// Header of the page
String htmlContent = renderTemplate(req);
// Subsctitution MESSAGE BOX
htmlContent = htmlContent.replaceAll("\\{" + KEY_ALERT_MESSAGE + "\\}", renderMessageBox(msg, msgType));
// Subsctitution FEATURE_ROWS
try {
htmlContent = htmlContent.replaceAll("\\{" + KEY_FEATURE_ROWS + "\\}", renderFeatureRows(ff4j, req));
} catch(IllegalArgumentException ieo) {
htmlContent = htmlContent.replaceAll("\\{" + KEY_FEATURE_ROWS + "\\}",
"Cannot render Features please check names (no $) '" + ieo.getMessage() + "'");
}
// substitution PROPERTIES_ROWS
try {
htmlContent = htmlContent.replaceAll("\\{" + KEY_PROPERTIES_ROWS + "\\}", renderPropertiesRows(ff4j, req));
} catch(IllegalArgumentException ieo) {
htmlContent = htmlContent.replaceAll("\\{" + KEY_PROPERTIES_ROWS + "\\}",
"Cannot render propertie please check names (no $) '" + ieo.getMessage() + "'");
}
// Substitution GROUP_LIST
String groups = ConsoleRenderer.renderGroupList(ff4j, MODAL_EDIT);
htmlContent = htmlContent.replaceAll("\\{" + KEY_GROUP_LIST_EDIT + "\\}", groups);
groups = groups.replaceAll(MODAL_EDIT, MODAL_CREATE);
htmlContent = htmlContent.replaceAll("\\{" + KEY_GROUP_LIST_CREATE + "\\}", groups);
groups = groups.replaceAll(MODAL_CREATE, MODAL_TOGGLE);
htmlContent = htmlContent.replaceAll("\\{" + KEY_GROUP_LIST_TOGGLE + "\\}", groups);
// Substitution PERMISSIONS
final String permissions = renderPermissionList(ff4j);
htmlContent = htmlContent.replaceAll("\\{" + KEY_PERMISSIONLIST + "\\}", permissions);
out.println(htmlContent);
}
/**
* Render the ff4f console webpage through different block.
*
* @param req
* http request (with parameters)
* @param res
* http response (with outouput test)
* @param msg
* text in the information box (blue/green/orange/red)
* @param msgType
* type of informatice message (info,success,warning,error)
* @throws IOException
* error during populating http response
*/
public static void renderPageMonitoring(FF4j ff4j, HttpServletRequest req, HttpServletResponse res, String msg, String msgType) throws IOException {
res.setContentType(CONTENT_TYPE_HTML);
PrintWriter out = res.getWriter();
String htmlContent = renderTemplateMonitoring(req);
htmlContent = htmlContent.replaceAll("\\{" + KEY_ALERT_MESSAGE + "\\}", renderMessageBox(msg, msgType));
htmlContent = htmlContent.replaceAll("\\{" + KEY_AUDIT_ROWS + "\\}", renderAuditRows(ff4j , req));
out.println(htmlContent);
}
/**
* Build info messages.
*
* @param featureName
* target feature name
* @param operationId
* target operationId
* @return
*/
public static String msg(String featureName, String operationId) {
return String.format("Feature <b>%s</b> has been successfully %s", featureName, operationId);
}
/**
* Build info messages.
*
* @param featureName
* target feature name
* @param operationId
* target operationId
* @return
*/
public static String renderMsgProperty(String featureName, String operationId) {
return String.format("Property <b>%s</b> has been successfully %s", featureName, operationId);
}
/**
* Build info messages.
*
* @param groupName
* target group name
* @param operationId
* target operationId
* @return
*/
public static String renderMsgGroup(String groupName, String operationId) {
return String.format("Group <b>%s</b> has been successfully %s", groupName, operationId);
}
/**
* Deliver CSS and Javascript files/
*
* @param req
* request
* @param res
* response
* @return value for resources
* @throws IOException
* exceptions
*/
public static boolean renderResources(HttpServletRequest req, HttpServletResponse res) throws IOException {
// Serve static resource file as CSS and Javascript
String resources = req.getParameter(RESOURCE);
if (resources != null && !resources.isEmpty()) {
if (RESOURCE_CSS_PARAM.equalsIgnoreCase(resources)) {
res.setContentType(CONTENT_TYPE_CSS);
res.getWriter().println(ConsoleRenderer.getCSS());
return true;
} else if (RESOURCE_JS_PARAM.equalsIgnoreCase(resources)) {
res.setContentType(CONTENT_TYPE_JS);
res.getWriter().println(ConsoleRenderer.getJS());
return true;
}
}
return false;
}
/**
* Display message box if message.
*
* @param message
* target message to display
* @param type
* type of messages
* @return html content to be displayed as message
*/
public static String renderMessageBox(String message, String type) {
StringBuilder sb = new StringBuilder();
// Display Message box
if (message != null && !message.isEmpty()) {
sb.append("<div class=\"alert alert-" + type + "\" >");
sb.append("<button type=\"button\" class=\"close\" data-dismiss=\"alert\">×</button>");
sb.append("<span style=\"font-style:normal;color:#696969;\">");
sb.append(message);
sb.append("</span>");
sb.append("</div>");
}
return sb.toString();
}
/**
* Load HTML template file and substitute by current URL context path
*
* @param req
* current http request
* @return current text part as string
*/
private static final String renderTemplate(HttpServletRequest req) {
if (htmlTemplate == null || htmlTemplate.isEmpty()) {
String ctx = req.getContextPath() + req.getServletPath() + "";
htmlTemplate = loadFileAsString(TEMPLATE_FILE);
htmlTemplate = htmlTemplate.replaceAll("\\{" + KEY_SERVLET_CONTEXT + "\\}", ctx);
htmlTemplate = htmlTemplate.replaceAll("\\{" + KEY_VERSION + "\\}", FF4J_VERSION);
}
return htmlTemplate;
}
/**
* Load HTML template file and substitute by current URL context path
*
* @param req
* current http request
* @return current text part as string
*/
private static final String renderTemplateMonitoring(HttpServletRequest req) {
if (htmlTemplateMonitoring == null || htmlTemplateMonitoring.isEmpty()) {
String ctx = req.getContextPath() + req.getServletPath() + "";
htmlTemplateMonitoring = loadFileAsString(TEMPLATE_FILE_MONITORING);
htmlTemplateMonitoring = htmlTemplateMonitoring.replaceAll("\\{" + KEY_SERVLET_CONTEXT + "\\}", ctx);
htmlTemplateMonitoring = htmlTemplateMonitoring.replaceAll("\\{" + KEY_VERSION + "\\}", FF4J_VERSION);
}
return htmlTemplateMonitoring;
}
public static String renderValue(String source, int column) {
StringBuilder sb = new StringBuilder();
source = source.replaceAll("\\\\", "/");
source = source.replaceAll("\\$", "$");
while (source.length() > column) {
sb.append(source.substring(0, column));
sb.append("\r\n<br>");
source = source.substring(column);
}
sb.append(source);
return sb.toString();
}
private static final String renderPropertiesRows(FF4j ff4j, HttpServletRequest req) {
StringBuilder sb = new StringBuilder();
final Map < String, Property<?>> mapOfProperties = ff4j.getProperties();
for(Map.Entry<String,Property<?>> uid : mapOfProperties.entrySet()) {
Property<?> currentProperty = uid.getValue();
sb.append("<tr>" + END_OF_LINE);
// Column with uid and description as tooltip
sb.append("<td><a class=\"ff4j-properties\" ");
if (null != currentProperty.getDescription()) {
sb.append(" tooltip=\"");
sb.append(currentProperty.getDescription());
sb.append("\"");
}
sb.append(">");
sb.append(renderValue(currentProperty.getName(), 50));
sb.append("</a>");
// Colonne Value
sb.append("</td><td>");
if (null != currentProperty.asString()) {
sb.append(renderValue(currentProperty.asString(), 60));
} else {
sb.append("--");
}
// Colonne Type
sb.append("</td><td>");
if (uxTypes.containsValue(currentProperty.getType())) {
sb.append(Util.getFirstKeyByValue(uxTypes, currentProperty.getType()));
} else {
sb.append(currentProperty.getType());
}
// Colonne Fixed Value
sb.append("</td><td>");
if (null != currentProperty.getFixedValues()) {
for (Object o : currentProperty.getFixedValues()) {
sb.append("<li>" + o.toString());
}
} else {
sb.append("--");
}
// Colonne Button Edit
sb.append("</td><td style=\"width:5%;text-align:center\">");
sb.append("<a data-toggle=\"modal\" href=\"#modalEditProperty\" data-pname=\"" + currentProperty.getName() + "\" ");
sb.append(" style=\"width:6px;\" class=\"open-EditPropertyDialog btn\">");
sb.append("<i class=\"icon-pencil\" style=\"margin-left:-5px;\"></i></a>");
// Colonne Button Delete
sb.append("</td><td style=\"width:5%;text-align:center\">");
sb.append("<a href=\"");
sb.append(req.getContextPath());
sb.append(req.getServletPath());
sb.append("?op=" + OP_RMV_PROPERTY + "&" + FEATID + "=" + currentProperty.getName());
sb.append("\" style=\"width:6px;\" class=\"btn\">");
sb.append("<i class=\"icon-trash\" style=\"margin-left:-5px;\"></i>");
sb.append("</a>");
sb.append("</td></tr>");
}
return sb.toString();
}
private static final String renderAuditRows(FF4j ff4j, HttpServletRequest req) {
StringBuilder sb = new StringBuilder();
EventRepository er = ff4j.getEventRepository();
EventQueryDefinition query = new EventQueryDefinition();
for (Event event : er.searchFeatureUsageEvents(query)) {
sb.append("<tr>" + END_OF_LINE);
sb.append("<td>" + SDF.format(new Date(event.getTimestamp())) + "</td>");
sb.append("<td>" + event.getType() + "</td>");
sb.append("<td>" + event.getName() + "</td>");
sb.append("<td>" + event.getAction() + "</td>");
sb.append("</tr>");
}
return sb.toString();
}
/**
* Produce the rows of the Feature Table.
*
* @param ff4j
* target ff4j.
* @param req
* current http request
* @return string representing the list of features
*/
private static final String renderFeatureRows(FF4j ff4j, HttpServletRequest req) {
StringBuilder sb = new StringBuilder();
final Map < String, Feature> mapOfFeatures = ff4j.getFeatures();
for(Map.Entry<String,Feature> uid : mapOfFeatures.entrySet()) {
Feature currentFeature = uid.getValue();
sb.append("<tr>" + END_OF_LINE);
// Column with uid and description as tooltip
sb.append("<td><a class=\"ff4j-tooltip\" ");
if (null != currentFeature.getDescription()) {
sb.append(" tooltip=\"");
sb.append(currentFeature.getDescription());
sb.append("\"");
}
sb.append(">");
sb.append(currentFeature.getUid());
sb.append("</a>");
// Colonne Group
sb.append("</td><td>");
if (null != currentFeature.getGroup()) {
sb.append(currentFeature.getGroup());
} else {
sb.append("--");
}
// Colonne Permissions
sb.append("</td><td>");
Set < String > permissions = currentFeature.getPermissions();
if (null != permissions && !permissions.isEmpty()) {
boolean first = true;
for (String perm : permissions) {
if (!first) {
sb.append(",");
}
sb.append(perm);
first = false;
}
} else {
sb.append("--");
}
// Colonne Strategy
sb.append("</td><td style=\"word-break: break-all;\">");
FlippingStrategy fs = currentFeature.getFlippingStrategy();
if (null != fs) {
sb.append(renderValue(fs.getClass().getCanonicalName(), 50));
if (fs.getInitParams() != null) {
for (Map.Entry<String, String> entry : fs.getInitParams().entrySet()) {
sb.append("<li>" + renderValue(entry.getKey() + " = " + entry.getValue(), 40));
}
}
} else {
sb.append("--");
}
// Colonne 'Holy' Toggle
sb.append("</td><td style=\"width:8%;text-align:center\">");
sb.append("<label class=\"switch switch-green\">");
sb.append("<input id=\"" + currentFeature.getUid() + "\" type=\"checkbox\" class=\"switch-input\"");
sb.append(" onclick=\"javascript:toggle(this)\" ");
if (currentFeature.isEnable()) {
sb.append(" checked");
}
sb.append(">");
sb.append("<span class=\"switch-label\" data-on=\"On\" data-off=\"Off\"></span>");
sb.append("<span class=\"switch-handle\"></span>");
sb.append("</label>");
// Colonne Button Edit
sb.append("</td><td style=\"width:5%;text-align:center\">");
sb.append("<a data-toggle=\"modal\" href=\"#modalEdit\" data-id=\"" + currentFeature.getUid() + "\" ");
sb.append(" data-desc=\"" + currentFeature.getDescription() + "\"");
sb.append(" data-group=\"" + currentFeature.getGroup() + "\"");
sb.append(" data-strategy=\"");
if (null != currentFeature.getFlippingStrategy()) {
sb.append(currentFeature.getFlippingStrategy().getClass().getCanonicalName());
}
sb.append("\" data-stratparams=\"");
if (null != currentFeature.getFlippingStrategy()) {
sb.append(currentFeature.getFlippingStrategy().getInitParams());
}
sb.append("\" data-permissions=\"");
if (null != currentFeature.getPermissions() && !currentFeature.getPermissions().isEmpty()) {
sb.append(currentFeature.getPermissions());
}
sb.append("\" style=\"width:6px;\" class=\"open-EditFlipDialog btn\">");
sb.append("<i class=\"icon-pencil\" style=\"margin-left:-5px;\"></i></a>");
// Colonne Button Delete
sb.append("</td><td style=\"width:5%;text-align:center\">");
sb.append("<a href=\"");
sb.append(req.getContextPath());
sb.append(req.getServletPath());
sb.append("?op=" + OP_RMV_FEATURE + "&" + FEATID + "=" + uid.getKey());
sb.append("\" style=\"width:6px;\" class=\"btn\">");
sb.append("<i class=\"icon-trash\" style=\"margin-left:-5px;\"></i>");
sb.append("</a>");
sb.append("</td></tr>");
}
return sb.toString();
}
/**
* Render group list block.
*
* @param ff4j
* target ff4j.
* @return list of group
*/
private static String renderGroupList(FF4j ff4j, String modalId) {
StringBuilder sb = new StringBuilder();
if (null != ff4j.getFeatureStore().readAllGroups()) {
for (String group : ff4j.getFeatureStore().readAllGroups()) {
sb.append("<li><a href=\"#\" onclick=\"\\$('\\#" + modalId + " \\#groupName').val('");
sb.append(group);
sb.append("');\">");
sb.append(group);
sb.append("</a></li>");
}
}
return sb.toString();
}
/**
* Render a permission list.
*
* @param ff4j
* reference to curent ff4j instance
* @return string representing the list of permissions
*/
private static String renderPermissionList(FF4j ff4j) {
StringBuilder sb = new StringBuilder("<br/>");
if (null != ff4j.getAuthorizationsManager()) {
for (String permission : ff4j.getAuthorizationsManager().listAllPermissions()) {
sb.append("\r\n<br/> <input type=\"checkbox\" ");
sb.append(" name=\"" + PREFIX_CHECKBOX + permission + "\"");
sb.append(" id=\"" + PREFIX_CHECKBOX + permission + "\" > ");
sb.append(permission);
}
}
return sb.toString();
}
/**
* Load the CSS File As String.
*
* @return CSS File
*/
private static final String getCSS() {
if (null == cssContent) {
cssContent = loadFileAsString(RESOURCE_CSS_FILE);
}
return cssContent;
}
/**
* Load the JS File As String.
*
* @return JS File
*/
private static final String getJS() {
if (null == jsContent) {
jsContent = loadFileAsString(RESOURCE_JS_FILE);
}
return jsContent;
}
/**
* Utils method to load a file as String.
*
* @param fileName
* target file Name.
* @return target file content as String
*/
private static String loadFileAsString(String fileName) {
InputStream in = ConsoleRenderer.class.getClassLoader().getResourceAsStream(fileName);
if (in == null) {
throw new IllegalArgumentException("Cannot load file " + fileName + " from classpath");
}
Scanner currentScan = null;
StringBuilder strBuilder = new StringBuilder();
try {
currentScan = new Scanner(in, UTF8_ENCODING);
while (currentScan.hasNextLine()) {
strBuilder.append(currentScan.nextLine());
strBuilder.append(NEW_LINE);
}
} finally {
if (currentScan != null) {
currentScan.close();
}
}
return strBuilder.toString();
}
}