/*
* Copyright 2008-2014 the original author or authors
*
* 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 org.kaleidofoundry.core.store;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import javax.ejb.Stateless;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.kaleidofoundry.core.lang.annotation.NotNull;
import org.kaleidofoundry.core.lang.annotation.Task;
import org.kaleidofoundry.core.util.StringHelper;
/**
* Simulate some Unix command (like head, tail, ..) on a text file content (like logging, traces...) <br/>
* <br/>
* It can be used as :
* <ul>
* <li>a classic class, that you instantiate</li>
* <li>an EJB service, then you benefit from the JavaEE resource injection</li>
* <li>as a rest web service, then you benefit from a portable rest web service</li>
* </ul>
*
* @author jraduget
*/
@Stateless(mappedName = "ejb/console/filestores")
@Path("/console/filestores/")
public class FileStoreConsoleController {
/**
* Enumeration of console operation
*/
public static enum Operation {
Head,
Tail,
Extract
}
/** Argument to specify the operation type */
public static final String OPERATION_ARGS = "operation";
/** Argument to specify index of begin line */
public static final String BEGINLINE_ARGS = "begin";
/** Argument to specify index of end line */
public static final String MAXLINE_COUNT_ARGS = "max";
/** Argument to specify the Charset encoding */
public static final String CHARSET_ARGS = "charset";
/** Argument to specify the highlight of a text in the output */
public static final String HIGHLIGHT_ARGS = "highlight";
/** Argument to specify if we add the line count as prefix to the output */
public static final String COUNTLINE_ARGS = "count";
/** Argument to specify how to cut the line length */
public static final String CUTLINE_ARGS = "cut";
/** Argument to specify if output must be rendered as html */
public static final String HTML_ARGS = "html";
/** The default line count result of a tail command. it will be used if {@link #MAXLINE_COUNT_ARGS} is not specified, */
public static final Long DEFAULT_MAXLINE_COUNT = 10L;
/** Set of all parameters names */
public static final Set<String> ARGS = Collections.synchronizedSet(new TreeSet<String>());
/** All registered resources */
public static final Set<String> REGISTERED_RESOURCES = Collections.synchronizedSet(new HashSet<String>());
static {
ARGS.add(BEGINLINE_ARGS);
ARGS.add(CHARSET_ARGS);
ARGS.add(MAXLINE_COUNT_ARGS);
ARGS.add(HIGHLIGHT_ARGS);
ARGS.add(OPERATION_ARGS);
ARGS.add(CUTLINE_ARGS);
ARGS.add(HTML_ARGS);
}
/** injected and used to handle security context */
@Context
SecurityContext securityContext;
/** injected and used to handle URIs */
@Context
UriInfo uriInfo;
/**
* @return console informations
*/
@GET
@Path("info")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public String info() {
StringBuilder str = new StringBuilder();
str.append("<p>");
str.append("<h2>Query parameters:</h2>");
str.append("<ul>");
str.append("<li>").append(HIGHLIGHT_ARGS).append("=...text to highlight...</li>");
str.append("<li>").append(CHARSET_ARGS).append("=UTF-8|ISO-8859-1|...</li>");
str.append("<li>").append(COUNTLINE_ARGS).append("=true|false</li>");
str.append("<li>").append(CUTLINE_ARGS).append("=120</li>");
str.append("<li>").append(HTML_ARGS).append("=true|false</li>");
str.append("</ul>");
str.append("</p>");
str.append("<p>");
str.append("<h2>Registered resources:</h2>");
str.append("<ul>");
for (String resource : REGISTERED_RESOURCES) {
str.append("<li>").append(resource).append("</li>");
}
str.append("</ul>");
str.append("</p>");
return str.toString();
}
/**
* add a resource to the console registry
*
* @param resource resource {@link URI}
* @throws ResourceNotFoundException
*/
public synchronized void register(final @NotNull URI resource) throws ResourceNotFoundException {
register(resource.toString());
}
/**
* add a resource to the console registry
*
* @param resource resource {@link URI}
* @throws ResourceNotFoundException
*/
@GET
@Path("register")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public synchronized String register(@QueryParam("resource") final String resource) throws ResourceNotFoundException {
if (!REGISTERED_RESOURCES.contains(resource)) {
boolean found = false;
// looking at registered file store
for (FileStore storeEntry : FileStoreFactory.getRegistry().values()) {
if (resource.toLowerCase().contains(storeEntry.getBaseUri().toLowerCase())) {
if (storeEntry.isUriManageable(resource)) {
found = true;
break;
}
}
}
if (found) {
REGISTERED_RESOURCES.add(resource);
} else {
// if no file store found try to create a new one
int localResourcePos = resource.lastIndexOf("/");
if (localResourcePos >= 0) {
// register a new file store to handle this resource
FileStoreFactory.provides(resource.substring(0, localResourcePos));
// the input console resource is register in console manager
REGISTERED_RESOURCES.add(resource);
} else {
throw new ResourceNotFoundException(resource);
}
}
}
return info();
}
/**
* remove a resource to the console registry
*
* @param resource
* @throws ResourceNotFoundException
*/
@GET
@Path("unregister")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public synchronized String unregister(@QueryParam("resource") final String resource) throws ResourceNotFoundException {
if (!REGISTERED_RESOURCES.contains(resource)) {
REGISTERED_RESOURCES.remove(resource);
return info();
} else {
throw new ResourceNotFoundException(resource);
}
}
/**
* head command on a file resource
*
* @param resource the resource which we want to extract the contents
* @return head of the file
* @throws ResourceException
* @throws IOException
*/
@GET
@Path("head")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public String head(@QueryParam("resource") final String resource) throws ResourceException, IOException {
Map<String, Serializable> parameters = new HashMap<String, Serializable>();
parameters.put(MAXLINE_COUNT_ARGS, DEFAULT_MAXLINE_COUNT);
return head(resource, addUriParameters(parameters));
}
/**
* head command on a file resource
*
* @param resource the resource which we want to extract the contents
* @param maxLineCount
* @return head of the file
* @throws ResourceException
* @throws IOException
*/
public String head(final String resource, final long maxLineCount) throws ResourceException, IOException {
final Map<String, Serializable> parameters = new HashMap<String, Serializable>();
parameters.put(OPERATION_ARGS, Operation.Tail);
if (maxLineCount >= 0) {
parameters.put(MAXLINE_COUNT_ARGS, Long.valueOf(maxLineCount));
}
return head(resource, addUriParameters(parameters));
}
/**
* head command on a file resource
*
* @param resource the resource which we want to extract the contents
* @param parameters
* @return head of the file
* @throws ResourceException
* @throws IOException
*/
public String head(final String resource, final Map<String, Serializable> parameters) throws ResourceException, IOException {
final Map<String, Serializable> typedParameters = typedParameters(parameters);
final Number maxLineCountArg = (Number) typedParameters.get(MAXLINE_COUNT_ARGS);
final long maxLine = maxLineCountArg != null ? maxLineCountArg.longValue() : DEFAULT_MAXLINE_COUNT;
final Reader reader = new InputStreamReader(in(resource, typedParameters).getInputStream());
final LinkedList<String> queue = new LinkedList<String>();
BufferedReader buffReader = null;
try {
long index = 0;
String currentLine;
buffReader = new BufferedReader(reader);
while ((currentLine = buffReader.readLine()) != null && queue.size() < maxLine) {
queue.add(currentLine);
index++;
}
return format(queue, typedParameters, index - queue.size()).toString();
} finally {
if (buffReader != null) {
buffReader.close();
}
}
}
/**
* tail command on a file resource
*
* @param resource the resource which we want to extract the contents
* @return tail of the file
* @throws ResourceException
* @throws IOException
*/
@GET
@Path("tail")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public String tail(@QueryParam("resource") final String resource) throws ResourceException, IOException {
Map<String, Serializable> parameters = new HashMap<String, Serializable>();
parameters.put(MAXLINE_COUNT_ARGS, DEFAULT_MAXLINE_COUNT);
return tail(resource, addUriParameters(parameters));
}
/**
* tail command on a file resource
*
* @param resource the resource which we want to extract the contents
* @param maxLineCount number of line you wish to keep in result buffer
* @return list of n last line of the buffer
* @throws IOException
* @throws ResourceException
*/
public String tail(final String resource, final long maxLineCount) throws ResourceException, IOException {
final Map<String, Serializable> args = new HashMap<String, Serializable>();
args.put(OPERATION_ARGS, Operation.Tail);
if (maxLineCount >= 0) {
args.put(MAXLINE_COUNT_ARGS, Long.valueOf(maxLineCount));
}
return tail(resource, addUriParameters(args));
}
/**
* tail command on a file resource
*
* @param resource the resource which we want to extract the contents
* @param parameters
* @return line of the buffer filter by args arguments
* @throws IOException
* @throws ResourceException
* @see #BEGINLINE_ARGS
* @see #MAXLINE_COUNT_ARGS
*/
public String tail(final String resource, final Map<String, Serializable> parameters) throws ResourceException, IOException {
final Map<String, Serializable> typedParameters = typedParameters(parameters);
final Number maxLineCountArg = (Number) typedParameters.get(MAXLINE_COUNT_ARGS);
final long maxLine = maxLineCountArg != null ? maxLineCountArg.longValue() : DEFAULT_MAXLINE_COUNT;
final Reader reader = new InputStreamReader(in(resource, typedParameters).getInputStream());
BufferedReader buffReader = null;
try {
int index = 0;
String currentLine;
LinkedList<String> queue = new LinkedList<String>();
buffReader = new BufferedReader(reader);
while ((currentLine = buffReader.readLine()) != null) {
if (queue.size() >= maxLine) {
queue.remove();
}
queue.offerLast(currentLine);
index++;
}
return format(queue, typedParameters, index - queue.size()).toString();
} finally {
if (buffReader != null) {
buffReader.close();
}
}
}
/**
* @param resource the resource which we want to extract the contents
* @param beginLine index of beginning line of file you wish
* @param maxLineCount index of the last line of file you wish
* @return list of n last line of the buffer
* @throws ResourceException
* @throws IOException
*/
@GET
@Path("extract")
@Produces({ MediaType.TEXT_PLAIN, MediaType.TEXT_HTML })
public String extract(@QueryParam("resource") final String resource, @QueryParam(BEGINLINE_ARGS) final long beginLine,
@QueryParam(MAXLINE_COUNT_ARGS) final long maxLineCount) throws ResourceException, IOException {
final Map<String, Serializable> parameters = new HashMap<String, Serializable>();
parameters.put(OPERATION_ARGS, Operation.Extract);
if (beginLine >= 0) {
parameters.put(BEGINLINE_ARGS, Long.valueOf(beginLine));
}
if (maxLineCount >= 0) {
parameters.put(MAXLINE_COUNT_ARGS, Long.valueOf(maxLineCount));
}
return extract(resource, addUriParameters(parameters));
}
/**
* @param resource the resource which we want to extract the contents
* @param parameters
* @return extract of the file
* @throws ResourceException
* @throws IOException
*/
public String extract(final String resource, final Map<String, Serializable> parameters) throws ResourceException, IOException {
final Map<String, Serializable> typepParameters = typedParameters(parameters);
final Number beginLineArg = (Number) typepParameters.get(BEGINLINE_ARGS);
final Number maxLineCountArg = (Number) typepParameters.get(MAXLINE_COUNT_ARGS);
final long beginLine = beginLineArg != null && beginLineArg.longValue() > 0 ? beginLineArg.longValue() : 1;
final long maxLine = maxLineCountArg != null ? maxLineCountArg.longValue() : DEFAULT_MAXLINE_COUNT;
final Charset charset = (Charset) typepParameters.get(CHARSET_ARGS);
Reader reader = null;
BufferedReader buffReader = null;
try {
String currentLine;
long index = 1;
LinkedList<String> queue = new LinkedList<String>();
if (charset == null) {
reader = new InputStreamReader(in(resource, typepParameters).getInputStream());
} else {
reader = new InputStreamReader(in(resource, typepParameters).getInputStream(), charset);
}
buffReader = new BufferedReader(reader);
while ((currentLine = buffReader.readLine()) != null && index < beginLine + maxLine) {
if (index >= beginLine) {
queue.add(currentLine);
}
index++;
}
return format(queue, typepParameters, index - 1 - queue.size()).toString();
} finally {
if (buffReader != null) {
buffReader.close();
}
}
}
/**
* Type the parameters input
*
* @param parameters the resource which we want to extract the contents
* @return Arguments correctly typed
* @throws IllegalArgumentException
*/
protected Map<String, Serializable> typedParameters(final Map<String, Serializable> parameters) throws IllegalArgumentException {
boolean isOk = false;
String msgErr = null;
final Map<String, Serializable> typedParameters = new HashMap<String, Serializable>();
// Begin line (optional)
{
final Serializable beginLine = parameters.get(BEGINLINE_ARGS);
if (beginLine != null) {
if (beginLine instanceof Number) {
isOk = true;
typedParameters.put(BEGINLINE_ARGS, beginLine);
} else if (beginLine instanceof String) {
try {
typedParameters.put(BEGINLINE_ARGS, Long.valueOf((String) beginLine));
isOk = true;
} catch (final NumberFormatException nbe) {
isOk = false;
msgErr = "Parameter '" + BEGINLINE_ARGS + "' must be java.lang.Number instance.";
}
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
// Max n line
{
final Serializable lastLineCount = parameters.get(MAXLINE_COUNT_ARGS);
if (lastLineCount != null) {
if (lastLineCount instanceof Number) {
isOk = true;
typedParameters.put(MAXLINE_COUNT_ARGS, lastLineCount);
} else if (lastLineCount instanceof String) {
try {
typedParameters.put(MAXLINE_COUNT_ARGS, Long.valueOf((String) lastLineCount));
isOk = true;
} catch (final NumberFormatException nbe) {
isOk = false;
msgErr = "Parameter '" + MAXLINE_COUNT_ARGS + "' must be java.lang.Number instance.";
}
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
// Highlight text
{
final Serializable highlight = parameters.get(HIGHLIGHT_ARGS);
if (highlight != null) {
if (highlight instanceof String) {
typedParameters.put(HIGHLIGHT_ARGS, highlight);
isOk = true;
} else {
isOk = false;
msgErr = "Parameter '" + HIGHLIGHT_ARGS + "' must be java.lang.String instance.";
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
// Encoding
{
final Serializable charset = parameters.get(CHARSET_ARGS);
if (charset != null) {
if (charset instanceof String) {
Charset.forName((String) charset); // throws illegal charset name if needed
typedParameters.put(CHARSET_ARGS, charset);
isOk = true;
} else {
isOk = false;
msgErr = "Parameter '" + CHARSET_ARGS + "' must be java.lang.String instance.";
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
// Add line count
{
final Serializable countLines = parameters.get(COUNTLINE_ARGS);
if (countLines != null) {
if (countLines instanceof Boolean) {
isOk = true;
typedParameters.put(COUNTLINE_ARGS, countLines);
} else {
if (countLines instanceof String) {
typedParameters.put(COUNTLINE_ARGS, Boolean.valueOf((String) countLines));
isOk = true;
} else {
isOk = false;
msgErr = "Parameter '" + COUNTLINE_ARGS + "' must be java.lang.Boolean instance.";
}
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
// Cut parameter
{
final Serializable cutNumber = parameters.get(CUTLINE_ARGS);
if (cutNumber != null) {
if (cutNumber instanceof Number) {
isOk = true;
typedParameters.put(CUTLINE_ARGS, cutNumber);
} else {
if (cutNumber instanceof String) {
typedParameters.put(CUTLINE_ARGS, Integer.valueOf((String) cutNumber));
} else {
isOk = false;
msgErr = "Parameter '" + CUTLINE_ARGS + "' must be java.lang.Integer instance.";
}
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
{
final Serializable html = parameters.get(HTML_ARGS);
if (html != null) {
if (html instanceof Boolean) {
isOk = true;
typedParameters.put(HTML_ARGS, html);
} else {
if (html instanceof String) {
typedParameters.put(HTML_ARGS, Boolean.valueOf((String) html));
isOk = true;
} else {
isOk = false;
msgErr = "Parameter '" + HTML_ARGS + "' must be java.lang.Boolean instance.";
}
}
if (!isOk) { throw new IllegalArgumentException(msgErr); }
}
}
return typedParameters;
}
/**
* resource input stream handler
*
* @param resource the resource which we want to extract the contents
* @param typedParameters
* @return handler on the resource
* @throws ResourceException
*/
protected ResourceHandler in(final String resource, final Map<String, Serializable> typedParameters) throws ResourceException {
// search in console resource registry
if (REGISTERED_RESOURCES.contains(resource)) {
// looking at registered file store
for (FileStore storeEntry : FileStoreFactory.getRegistry().values()) {
if (resource.toLowerCase().contains(storeEntry.getBaseUri().toLowerCase())) { return storeEntry.get(resource.substring(storeEntry.getBaseUri()
.length())); }
}
}
throw new ResourceNotFoundException(resource);
}
/**
* format output queue
*
* @param queue
* @param parameters
* @param fromIndex
* @return formated output (highlight, line count, encoding, cut parameter)
*/
@Task(comment = "formated output : highlight, cut parameter")
protected StringBuilder format(final LinkedList<String> queue, final Map<String, Serializable> parameters, long fromIndex) {
final StringBuilder buffer = new StringBuilder();
Boolean countLine = (Boolean) parameters.get(COUNTLINE_ARGS);
String highlightText = (String) parameters.get(HIGHLIGHT_ARGS);
int prefixLength = String.valueOf(fromIndex + queue.size()).length() + 1;
boolean highlight = !StringHelper.isEmpty(highlightText);
Number cutLine = (Number) parameters.get(CUTLINE_ARGS);
Boolean html = (Boolean) parameters.get(HTML_ARGS);
if (countLine == null) {
countLine = Boolean.FALSE;
}
if (html == null) {
html = Boolean.FALSE;
}
for (String line : queue) {
if (countLine) {
buffer.append(StringHelper.leftPad(String.valueOf(++fromIndex), prefixLength, '0')).append(" ");
}
if (highlight && html) {
line = StringHelper.replaceAll(line, highlightText, "<span style=\"background-color:yellow;\">" + highlightText + "</span>");
}
buffer.append(cutLine == null ? line : StringHelper.truncate(line, cutLine.intValue()));
buffer.append(html ? "<br/>" : "\n");
}
return buffer;
}
/**
* add uri parameter (if needed)
*
* @return enriched the input parameters with url parameters
*/
protected Map<String, Serializable> addUriParameters(final Map<String, Serializable> parameters) {
if (uriInfo != null) {
for (Entry<String, List<String>> queryParam : uriInfo.getQueryParameters().entrySet()) {
if (queryParam.getValue().size() == 1) {
parameters.put(queryParam.getKey(), queryParam.getValue().get(0));
} else {
parameters.put(queryParam.getKey(), String.valueOf(queryParam.getValue()));
}
}
}
return parameters;
}
}