/**
* Copyright 2014 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.admin.details;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import com.google.common.net.HttpHeaders;
import io.dropwizard.jetty.setup.ServletEnvironment;
import io.soabase.admin.auth.AuthFields;
import io.soabase.admin.auth.AuthFilter;
import io.soabase.admin.auth.AuthSpec;
import io.soabase.admin.components.ComponentManager;
import io.soabase.admin.components.MetricComponent;
import io.soabase.admin.components.TabComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Calendar;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public class IndexServlet extends HttpServlet
{
public static final String SOA_TAB_PREFIX = "soa-tab-";
private static final String FORCE = "/force";
private final Logger log = LoggerFactory.getLogger(getClass());
private final ComponentManager componentManager;
private final AuthSpec authSpec;
private final List<IndexMapping> mappings;
private final AtomicReference<Map<String, Entry>> files = new AtomicReference<Map<String, Entry>>(Maps.<String, Entry>newHashMap());
private final AtomicLong lastModified = new AtomicLong(0);
private final AtomicInteger builtFromVersion = new AtomicInteger(-1);
private final ObjectMapper mapper = new ObjectMapper();
private static class Entry
{
final String content;
final String eTag;
public Entry(String content, String eTag)
{
this.content = content;
this.eTag = eTag;
}
}
public IndexServlet(ComponentManager componentManager, List<IndexMapping> mappings, AuthSpec authSpec)
{
this.componentManager = componentManager;
this.authSpec = authSpec;
this.mappings = ImmutableList.copyOf(mappings);
}
public void setServlets(ServletEnvironment servlets)
{
AuthFilter authFilter = (authSpec != null) ? new AuthFilter(authSpec) : null;
for ( IndexMapping mapping : mappings )
{
String name = Splitter.on('.').split(mapping.getFile()).iterator().next();
servlets.addServlet(name, this).addMapping(mapping.getPath());
servlets.addServlet(name + "-force", this).addMapping(FORCE + mapping.getPath());
if ( !mapping.isAuthServlet() && (authFilter != null) )
{
servlets.addFilter("auth-" + name, authFilter).addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, mapping.getPath());
servlets.addFilter("auth-" + name + "-force", authFilter).addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, FORCE + mapping.getPath());
}
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String requestURI = ((request.getRequestURI() != null) && (request.getRequestURI().length() > 0)) ? request.getRequestURI() : "/";
if ( requestURI.startsWith(FORCE) )
{
rebuild();
requestURI = (requestURI.length() > FORCE.length()) ? requestURI.substring(FORCE.length()) : "/";
}
else
{
int componentManagerVersion = componentManager.getVersion();
int localBuiltFromVersion = builtFromVersion.get();
if ( localBuiltFromVersion != componentManagerVersion )
{
if ( builtFromVersion.compareAndSet(localBuiltFromVersion, componentManagerVersion) )
{
rebuild();
}
}
}
Entry entry = files.get().get(requestURI);
if ( entry == null )
{
response.setStatus(404);
return;
}
response.setStatus(200);
response.setContentType("text/html");
response.setContentLength(entry.content.length());
response.setCharacterEncoding("UTF-8");
response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModified.get());
response.setHeader(HttpHeaders.ETAG, entry.eTag);
response.getWriter().print(entry.content);
}
private synchronized void rebuild()
{
Map<String, Entry> newFiles = Maps.newHashMap();
for ( IndexMapping mapping : mappings )
{
try
{
String content = Resources.toString(Resources.getResource(mapping.getFile()), Charsets.UTF_8);
newFiles.put(mapping.getKey(), new Entry(content, "")); // temp entry
}
catch ( IOException e )
{
log.error("Could not load resource: " + mapping.getFile(), e);
throw new RuntimeException(e);
}
}
try
{
doReplacements(componentManager, newFiles);
}
catch ( IOException e )
{
throw new RuntimeException(e);
}
// zero out the millis since the date we get back from If-Modified-Since will not have them
lastModified.set((System.currentTimeMillis() / 1000) * 1000);
files.set(newFiles);
builtFromVersion.set(componentManager.getVersion());
}
private void doReplacements(ComponentManager componentManager, Map<String, Entry> files) throws IOException
{
int currentYear = Calendar.getInstance().get(Calendar.YEAR);
String firstId = "";
StringBuilder tabsBuilder = new StringBuilder();
StringBuilder idsBuilder = new StringBuilder();
StringBuilder cssBuilder = new StringBuilder();
StringBuilder jsBuilder = new StringBuilder();
StringBuilder tabContentBuilder = new StringBuilder();
StringBuilder metricsBuilder = new StringBuilder();
StringBuilder authFieldsBuilder = new StringBuilder();
for ( TabComponent tab : componentManager.getTabs() )
{
if ( firstId.length() == 0 )
{
firstId = tab.getId();
}
String id = SOA_TAB_PREFIX + tab.getId();
tabsBuilder.append("<li id='").append(id).append("-li").append("'><a href=\"#").append(tab.getId()).append("\">").append(tab.getName()).append("</a></li>\n");
idsBuilder.append("soaTabIds.push('").append(id).append("');\n");
for ( String cssFile : tab.getCssUris() )
{
cssBuilder.append("<link rel=\"stylesheet\" href=\"").append(cssFile).append("\">\n");
}
for ( String jssFile : tab.getJavascriptUris() )
{
jsBuilder.append("<script src=\"").append(jssFile).append("\"></script>\n");
}
String tabContent = Resources.toString(Resources.getResource(tab.getContentResourcePath()), Charsets.UTF_8);
tabContentBuilder.append("<div class=\"soa-hidden\" id=\"" + SOA_TAB_PREFIX).append(tab.getId()).append("\">").append(tabContent).append("</div>\n");
}
for ( MetricComponent metric : componentManager.getMetrics() )
{
String obj = mapper.writeValueAsString(metric);
metricsBuilder.append("vmMetrics.push(").append(obj).append(");\n");
}
String signInHeading = "";
String signInButton = "";
if ( authSpec != null )
{
signInHeading = authSpec.getSignInHeading();
signInButton = authSpec.getSignInButton();
for ( AuthFields field : AuthFields.values() )
{
if ( authSpec.getFields().contains(field) )
{
authFieldsBuilder.append("$('#").append(field.name().toLowerCase()).append("').removeClass('soa-hidden');\n");
}
else
{
authFieldsBuilder.append("$('#").append(field.name().toLowerCase()).append("').remove();\n");
}
}
}
String tabs = tabsBuilder.toString();
String metrics = metricsBuilder.toString();
String tabContent = tabContentBuilder.toString();
String ids = idsBuilder.toString();
String css = cssBuilder.toString();
String js = jsBuilder.toString();
String authFields = authFieldsBuilder.toString();
for ( IndexMapping mapping : mappings )
{
Entry entry = files.get(mapping.getKey());
String content = entry.content;
content = content.replace("$SOA_HAS_AUTH$", (authSpec != null) ? "true" : "false");
content = content.replace("$SOA_TABS$", tabs);
content = content.replace("$SOA_TABS_CONTENT$", tabContent);
content = content.replace("$SOA_METRICS$", metrics);
content = content.replace("$SOA_DEFAULT_TAB_ID$", firstId);
content = content.replace("$SOA_TAB_IDS$", ids);
content = content.replace("$SOA_CSS$", css);
content = content.replace("$SOA_JS$", js);
content = content.replace("$SOA_NAME$", componentManager.getAppName());
content = content.replace("$SOA_COPYRIGHT$", "" + currentYear + " " + componentManager.getCompanyName());
content = content.replace("$SOA_FOOTER_MESSAGE$", "" + componentManager.getFooterMessage());
content = content.replace("$SOA_AUTH_FIELDS$", authFields);
content = content.replace("$SOA_AUTH_HEADING$", signInHeading);
content = content.replace("$SOA_AUTH_BUTTON$", signInButton);
String eTag = '"' + Hashing.murmur3_128().hashBytes(content.getBytes()).toString() + '"';
files.put(mapping.getKey(), new Entry(content, eTag));
}
}
}