Format links in logback - logback

I am using groovy configuration with logback. Occasionally, I will log a directory or file location, and I'd like it to come up in my HTML log as a link. Here is what my configuration looks like currently.
appender("htmlLog", FileAppender) {
file = "${logPath}/${logName}.html"
append = false
encoder(LayoutWrappingEncoder) {
layout("ch.qos.logback.classic.html.HTMLLayout"){
pattern = "%d{yyyy/MM/dd HH:mm:ss}%-5p%logger{0}%m"
}
}
}
Anyone have a thought as to how I could get this?

There are two obstacles to generating anchor tags or any other HTML within the table. I'm working against logback 1.2.3
First you need a way to convert your message, looking for paths and replacing them with anchors. Creating custom converters that you can use from the pattern is straightforward and documented here. My crude implementation looks like this, you'll probably want to modify the path detection to suit you:
package ch.qos.logback.classic.html;
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.helpers.Transform;
public class LinkConverter extends ClassicConverter {
public String convert(ILoggingEvent iLoggingEvent) {
String message = iLoggingEvent.getMessage();
message = Transform.escapeTags(message);
message = message.replaceAll(" (/\\S+)", " file://$1");
return message;
}
}
This is attempting to escape any suspicious characters before replacing strings like /path/to/thing with an anchor tag.
Secondly, the HTMLLayout escapes everything, this is so it doesn't generate a malformed table and improves security (scripts can't be injected etc). So even with your new converter wired up and referenced correctly HTMLLayout will escape the anchor.
To get around this I extended HTMLLayout, unfortunately you have to override the guts of the class and put it in the same package to access package private fields.
All you want to change is the escaping line, I changed it to String s = c.getClass().equals(LinkConverter.class) ? c.convert(event): Transform.escapeTags(c.convert(event)); to try and minimise the impact.
Here is the full implementation:
package ch.qos.logback.classic.html;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.helpers.Transform;
import ch.qos.logback.core.pattern.Converter;
public class UnsafeHTMLLayout extends HTMLLayout{
public String doLayout(ILoggingEvent event) {
StringBuilder buf = new StringBuilder();
this.startNewTableIfLimitReached(buf);
boolean odd = true;
if((this.counter++ & 1L) == 0L) {
odd = false;
}
String level = event.getLevel().toString().toLowerCase();
buf.append(CoreConstants.LINE_SEPARATOR);
buf.append("<tr class=\"");
buf.append(level);
if(odd) {
buf.append(" odd\">");
} else {
buf.append(" even\">");
}
buf.append(CoreConstants.LINE_SEPARATOR);
for(Converter c = this.head; c != null; c = c.getNext()) {
this.appendEventToBuffer(buf, c, event);
}
buf.append("</tr>");
buf.append(CoreConstants.LINE_SEPARATOR);
if(event.getThrowableProxy() != null) {
this.throwableRenderer.render(buf, event);
}
return buf.toString();
}
private void appendEventToBuffer(StringBuilder buf, Converter<ILoggingEvent> c, ILoggingEvent event) {
buf.append("<td class=\"");
buf.append(this.computeConverterName(c));
buf.append("\">");
String s = c.getClass().equals(LinkConverter.class) ? c.convert(event): Transform.escapeTags(c.convert(event));
buf.append(s);
buf.append("</td>");
buf.append(CoreConstants.LINE_SEPARATOR);
}
}
My final logback configuration looks like this:
import ch.qos.logback.classic.html.LinkConverter
conversionRule("linkEscaper", LinkConverter.class)
appender("htmlLog", FileAppender) {
file = "/tmp/out.html"
append = false
encoder(LayoutWrappingEncoder) {
layout("ch.qos.logback.classic.html.UnsafeHTMLLayout"){
pattern = "%d{yyyy/MM/dd HH:mm:ss}%-5p%logger{0}%linkEscaper"
}
}
}
root(INFO, ["htmlLog"])
Here's my repo with this code.

Related

How can I export my bookmarks and import them into a table?

I have hundreds of bookmarks that I have collected over the years that I would like to put into a searchable table with extra information such as categories, types, descriptions etc.
My first attempt at this was manually putting them into a JSON file and then using the DataTables plug-in to display them, however, this was tedious and time-consuming.
The second attempt was to use Wordpress and use Advanced Custom Fields to do this but again still quite tedious.
Obviously, I can export my bookmarks as an HTML file and I'm considering editing and styling this file to suit my needs but it is absolutely massive and also has loads of extraneous information. I've been trying to use CSV conversions of this file to import it into various Wordpress plug-ins that say they offer this exact functionality to know avail. I've also tried doing something similar with firefox's backup that exports to a JSON file but again no luck.
I know that I will have to manually put in some of the information, but I'm trying to cut down on the workload by about a third. Am I going about this the wrong way? Is it even possible? Just wondering if anyone out there has tried to do the same thing and how they went about it.
That was a lovely challenge, thanks. Basically, what I've done is saved the exported bookmarks as HTML and then created a simple page with an empty table. Then my JS does this:
$(function() {
var example = $("#example").DataTable({
"responsive": true,
"columns": [
{
"title": "Title",
"data": "text"
},{
"title": "Date added",
"data": "date",
"render": function(d){
return moment(d, "X").format("DD/MM/YYYY");
}
},{
"title": "URI",
"data": "href",
"render": function(d){
return $("<a></a>",{
"text": d,
"href": d
}).prop("outerHTML");
}
}
],
"initComplete": function(settings, json) {
$.get("bookmarks_12_2_16.html", function( data ) {
$(data).find("dl").children("dt").children("a").each(function(k, v){
if(!~~$(v).attr("href").indexOf("http")){
example.row.add({
"href": $(v).attr("href"),
"text": $(v).text(),
"date": $(v).attr("add_date")
});
}
});
example.draw();
});
}
});
});
Basically it gets the HTML and iterates over the dts within the dl and, if the href is http or https, it adds it to the table with the correct date (you'd date function might have to be different seeing as I'm in the UK and I'm using momentjs). Hope that helps.
You can parse exported file from chrome using below:
Here i have used SAX parser to parse and extract url and link from bookmark.
Below three classes will parse xml and print bookmark url title and link.
you can export it into csv or you can use it in better way to generate table dynamically from which you can search.
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
public class BookmarkReader {
public static void main(String argv[]) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
try {
xmlReader.setFeature(
"http://apache.org/xml/features/continue-after-fatal-error",
true);
} catch (SAXException e) {
System.out.println("error in setting up parser feature");
}
xmlReader.setContentHandler(new ContentHandler());
xmlReader.setErrorHandler(new MyErrorHandler());
xmlReader.parse("C:\\Users\\chetankumar.p\\Documents\\bookmarks_12_2_16.html");
} catch (Throwable e) {
System.out.println(e.getMessage());
}
}
}
import java.util.ArrayList;
import java.util.List;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class ContentHandler extends DefaultHandler {
class Bookmark {
public String title;
public String href;
}
Bookmark bookmark;
List<Bookmark> bookmarks = new ArrayList<>();
#Override
public void endDocument() throws SAXException {
for (Bookmark bookmark1 : bookmarks) {
System.out.println("title : " + bookmark1.title);
System.out.println("title : " + bookmark1.href);
}
}
#Override
public void startElement(String uri, String localName,
String qName, Attributes attributes)
throws SAXException {
if (qName.equalsIgnoreCase("a")) {
bookmark = new Bookmark();
System.out.println("href ::: " + attributes.getValue("HREF"));
bookmark.href = attributes.getValue("HREF");
}
}
#Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equalsIgnoreCase("a")) {
bookmarks.add(bookmark);
bookmark = null;
}
}
#Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (bookmark != null) {
bookmark.title = new String(ch, start, length);
}
}
}
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
public class MyErrorHandler implements ErrorHandler {
private String getParseExceptionInfo(SAXParseException spe) {
String systemId = spe.getSystemId();
if (systemId == null) {
systemId = "null";
}
String info = "URI=" + systemId + " Line="
+ spe.getLineNumber() + ": " + spe.getMessage();
return info;
}
public void warning(SAXParseException spe) throws SAXException {
System.out.println("Warning: " + getParseExceptionInfo(spe));
}
public void error(SAXParseException spe) throws SAXException {
String message = "Error: " + getParseExceptionInfo(spe);
System.out.println(message);
}
public void fatalError(SAXParseException spe) throws SAXException {
String message = "Fatal Error: " + getParseExceptionInfo(spe);
System.out.println(message);
}
}
Firefox Bookmarks to .xlsm [SOLVED]
I figured out how to get my bookmarks out of Firefox and into Excel. A macro processes them, and other macros get you around the Worksheet.
It uses: 1.) Firefox, 2.) SQLite3, 3.) DOS, 4.) Excel VBA, 5.) NirCmd (optional)
All you have to do is to categorize your bookmarks, and then click the sort button.
These two PDFs provide code for the BAT files, and explain how to set up the paths for the macros. More instructions are in the VBA of the xlsm. These three files are on my Google Docs.
I thought I'd share this, because it's cool, might be useful to others . . . and maybe someone can improve on what I have, or give me some more ideas . . . maybe this post can be left open.
bookmarks-to-CSV.pdf . . . This first page provides all you need to know, to get your Bookmarks into a .CSV file (This won't do anything to your actual Bookmarks, it just extracts a copy of them.) . . . Then, the next few pages provide the details of what's going on . . . https://drive.google.com/open?id=1xYWPQtijqCzk-1nzTsTb0ZUVKYJNFokR
Custom UI Editor.pdf . . . Supporting info, and BAT file code to be set up . . . https://drive.google.com/open?id=1G2AWBamOrbAo2ZNDtUyYdzegjKzz34DA
bookmarks-p.xlsm . . . The Preview on Google Docs isn't that great. Some supporting info is in the first four Worksheets. Bookmarks are in the fifth Worksheet (see tabs across the bottom) . . . https://drive.google.com/open?id=1ZOuOBkdJjMx1T4xMUNG7sf6MDWurUqqy
Use extensions on the Chrome Store to generate a JSON/CSV output. Converting that to table form should be fairly straightforward.
Bookmarks to JSON/CSV (including chrome history)
Bookmarks to
JSON (root or folder based)
Bookmarks Table (view only, by date)

Camel bindy marshal to file creates multiple header row

I have the following camel route:
from(inputDirectory)
.unmarshal(jaxb)
.process(jaxb2CSVDataProcessor)
.split(body()) //because there is a list of CSVRecords
.marshal(bindyCsvDataFormat)
.to(outputDirectory); //appending to existing file using "?autoCreate=true&fileExist=Append"
for my CSV model class I am using annotations:
#CsvRecord(separator = ",", generateHeaderColumns = true)
...
and for properties
#DataField(pos = 0)
...
My problem is that the headers are appended every time a new csv record is appended.
Is there a non-dirty way to control this? Am I missing anything here?
I made a work around which is working quite nicely, creating the header by querying the columnames of the #DataField annotation. This is happening once the first time the file is written. I wrote down the whole solution here:
How to generate a Flat file with header and footer using Camel Bindy
I ended up adding a processor that checks if the csv file exists just before the "to" clause. In there I do a manipulation of the byte array and remove the headers.
Hope this helps anyone else. I needed to do something similar where after my first split message I wanted to supress the header output. Here is a complete class (the 'FieldUtils' is part of the apache commons lib)
package com.routes;
import java.io.OutputStream;
import org.apache.camel.Exchange;
import org.apache.camel.dataformat.bindy.BindyAbstractFactory;
import org.apache.camel.dataformat.bindy.BindyCsvFactory;
import org.apache.camel.dataformat.bindy.BindyFactory;
import org.apache.camel.dataformat.bindy.FormatFactory;
import org.apache.camel.dataformat.bindy.csv.BindyCsvDataFormat;
import org.apache.commons.lang3.reflect.FieldUtils;
public class StreamingBindyCsvDataFormat extends BindyCsvDataFormat {
public StreamingBindyCsvDataFormat(Class<?> type) {
super(type);
}
#Override
public void marshal(Exchange exchange, Object body, OutputStream outputStream) throws Exception {
final StreamingBindyModelFactory factory = (StreamingBindyModelFactory) super.getFactory();
final int splitIndex = exchange.getProperty(Exchange.SPLIT_INDEX, -1, int.class);
final boolean splitComplete = exchange.getProperty(Exchange.SPLIT_COMPLETE, false, boolean.class);
super.marshal(exchange, body, outputStream);
if (splitIndex == 0) {
factory.setGenerateHeaderColumnNames(false); // turn off header generate after first exchange
} else if(splitComplete) {
factory.setGenerateHeaderColumnNames(true); // turn on header generate when split complete
}
}
#Override
protected BindyAbstractFactory createModelFactory(FormatFactory formatFactory) throws Exception {
BindyCsvFactory bindyCsvFactory = new StreamingBindyModelFactory(getClassType());
bindyCsvFactory.setFormatFactory(formatFactory);
return bindyCsvFactory;
}
public class StreamingBindyModelFactory extends BindyCsvFactory implements BindyFactory {
public StreamingBindyModelFactory(Class<?> type) throws Exception {
super(type);
}
public void setGenerateHeaderColumnNames(boolean generateHeaderColumnNames) throws IllegalAccessException {
FieldUtils.writeField(this, "generateHeaderColumnNames", generateHeaderColumnNames, true);
}
}
}

Type preservation failing with RemoteClass

Consider the following example dataclass:
[RemoteClass]
public class SOTestData {
public var i:int;
public function SOTestData(i:int) {
this.i = i;
}
}
As I understand, the RemoteClass metadata-tag should ensure that when an object of this class gets sreialized, the type information is preserved.
I used the following program to test:
public class SOTest extends Sprite {
public function SOTest() {
var data:SharedObject = SharedObject.getLocal("SOTest");
if (data.data.object) {
try {
var stored:SOTestData = data.data.object;
trace(stored.i);
} finally {
data.clear();
}
}
else {
data.data.object = new SOTestData(15);
data.flush();
}
}
}
Here the first run writes the data, seconds reads and clears. Running this, I still get a class cast error. Indeed, in the SharedObject there is no type information stored.
I don't think i'm using the metadata wrong, could it maybe be that the compiler doesn't know what to do with it? I don't get any compiler errors/warnings, although when i use some inexistant tag it doesn't complain either. I'm using Flex 4.6 SDK with FlashDevelop as IDE.
EDIT:
Below is the shared object. As you can see, the type is saved as "Object" instead of the actual type.
so = [object #2, class 'SharedObject'] {
data: [object #0, class 'Object'] {
object: [object #1, class 'Object', dynamic 'False', externalizable 'False'] {
i: 15,
},
}
}
I've only used RemoteClass for making AMF RemoteObject calls; I didn't think it had anything to do w/ Shared Objects. Per the docs
Use the [RemoteClass] metadata tag to register the class with Flex so
that Flex preserves type information when a class instance is
serialized by using Action Message Format (AMF). You insert the
[RemoteClass] metadata tag before an ActionScript class definition.
The [RemoteClass] metadata tag has the following syntax:
As best I can tell from the code you provided, you are not serializing the object in AMF format.
I believe your class cast error is due to the fact that you aren't casting your class. Shared Objects always come back as generic Objects. Try this:
var stored:SOTestData = data.data.object as SOTestData ;
Here is some code from an application I use. First the value object which will get serialized in a shared object:
package com.login.vos
{
[RemoteClass(alias="com.login.vos.UserVO")]
public class UserVO
{
public function UserVO()
{
}
public var firstName :String;
public var lastName :String;
public var userID :Number;
}
}
The the code to save the object:
public static function saveUserVO(userVO:UserVO):void{
var userSharedObject :SharedObject = SharedObject.getLocal('userVO') ;
userSharedObject.data.userVO = userVO;
userSharedObject.flush();
}
And finally, the code to load the objecT:
public static function getUserVO():UserVO{
var userSharedObject :SharedObject = SharedObject.getLocal('userVO')
if(userSharedObject.size <=0){
return null;
}
return userSharedObject.data.userVO as UserVO;
}
The only obvious difference between this and the code by the original poster is that I'm specifying an alias in the RemoteClass metadata.

Deserializing from JSON back to joda DateTime in Play 2.0

I can't figure out the magic words to allow posting JSON for a DateTime field in my app. When queried, DateTimes are returned as microseconds since the epoch. When I try to post in that format though ({"started":"1341006642000","task":{"id":1}}), I get "Invalid value: started".
I also tried adding #play.data.format.Formats.DateTime(pattern="yyyy-MM-dd HH:mm:ss") to the started field and posting {"started":"2012-07-02 09:24:45","task":{"id":1}} which had the same result.
The controller method is:
#BodyParser.Of(play.mvc.BodyParser.Json.class)
public static Result create(Long task_id) {
Form<Run> runForm = form(Run.class).bindFromRequest();
for (String key : runForm.data().keySet()) {
System.err.println(key + " => " + runForm.apply(key).value() + "\n");
}
if (runForm.hasErrors())
return badRequest(runForm.errorsAsJson());
Run run = runForm.get();
run.task = Task.find.byId(task_id);
run.save();
ObjectNode result = Json.newObject();
result.put("id", run.id);
return ok(result);
}
I can also see from the output that the values are being received correctly. Anyone know how to make this work?
After reading the "Register a custom DataBinder" section of the Handling form submission page along with the Application global settings page and comparing with this question I came up with the following solution:
I created a custom annotation with an optional format attribute:
package models;
import java.lang.annotation.*;
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#play.data.Form.Display(name = "format.joda.datetime", attributes = { "format" })
public #interface JodaDateTime {
String format() default "";
}
and registered a custom formatter from onStart:
import java.text.ParseException;
import java.util.Locale;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import play.*;
import play.data.format.Formatters;
public class Global extends GlobalSettings {
#Override
public void onStart(Application app) {
Formatters.register(DateTime.class, new Formatters.AnnotationFormatter<models.JodaDateTime,DateTime>() {
#Override
public DateTime parse(models.JodaDateTime annotation, String input, Locale locale) throws ParseException {
if (input == null || input.trim().isEmpty())
return null;
if (annotation.format().isEmpty())
return new DateTime(Long.parseLong(input));
else
return DateTimeFormat.forPattern(annotation.format()).withLocale(locale).parseDateTime(input);
}
#Override
public String print(models.JodaDateTime annotation, DateTime time, Locale locale) {
if (time == null)
return null;
if (annotation.format().isEmpty())
return time.getMillis() + "";
else
return time.toString(annotation.format(), locale);
}
});
}
}
You can specify a format if you want, or it will use milliseconds since the epoch by default. I was hoping there would be a simpler way since Joda is included with the Play distribution, but this got things working.
Note: you'll need to restart your Play app as it doesn't seem to detect changes to the Global class.

Using json with Play 2

I'm trying to create a simple application that allows me to create, read, update and delete various users. I have a basic UI-based view, controller and model that work, but wanted to be more advanced than this and provide a RESTful json interface.
However, despite reading everything I can find in the Play 2 documentation, the Play 2 Google groups and the stackoverflow website, I still can't get this to work.
I've updated my controller based on previous feedback and I now believe it is based on the documentation.
Here is my updated controller:
package controllers;
import models.Member;
import play.*;
import play.mvc.*;
import play.libs.Json;
import play.data.Form;
public class Api extends Controller {
/* Return member info - version to serve Json response */
public static Result member(Long id){
ObjectNode result = Json.newObject();
Member member = Member.byid(id);
result.put("id", member.id);
result.put("email", member.email);
result.put("name", member.name);
return ok(result);
}
// Create a new body parser of class Json based on the values sent in the POST
#BodyParser.Of(Json.class)
public static Result createMember() {
JsonNode json = request().body().asJson();
// Check that we have a valid email address (that's all we need!)
String email = json.findPath("email").getTextValue();
if(name == null) {
return badRequest("Missing parameter [email]");
} else {
// Use the model's createMember class now
Member.createMember(json);
return ok("Hello " + name);
}
}
....
But when I run this, I get the following error:
incompatible types [found: java.lang.Class<play.libs.Json>] [required: java.lang.Class<?extends play.mvc.BodyParser>]
In /Users/Mark/Development/EclipseWorkspace/ms-loyally/loyally/app/controllers/Api.java at line 42.
41 // Create a new body parser of class Json based on the values sent in the POST
42 #BodyParser.Of(Json.class)
43 public static Result createMember() {
44 JsonNode json = request().body().asJson();
45 // Check that we have a valid email address (that's all we need!)
46 String email = json.findPath("email").getTextValue();
As far as I can tell, I've copied from the documentation so I would appreciate any help in getting this working.
There appear to be conflicts in the use of the Json class in the Play 2 documentation. To get the example above working correctly, the following imports are used:
import play.mvc.Controller;
import play.mvc.Result;
import play.mvc.BodyParser;
import play.libs.Json;
import play.libs.Json.*;
import static play.libs.Json.toJson;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ObjectNode;
#BodyParser.Of(play.mvc.BodyParser.Json.class)
public static index sayHello() {
JsonNode json = request().body().asJson();
ObjectNode result = Json.newObject();
String name = json.findPath("name").getTextValue();
if(name == null) {
result.put("status", "KO");
result.put("message", "Missing parameter [name]");
return badRequest(result);
} else {
result.put("status", "OK");
result.put("message", "Hello " + name);
return ok(result);
}
}
Note the explicit calling of the right Json class in #BodyParser
I'm not sure if this is a bug or not? But this is the only way I could get the example to work.
Import those two
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
According to this documentation: http://fasterxml.github.io/jackson-databind/javadoc/2.0.0/com/fasterxml/jackson/databind/node/ObjectNode.html
Try this:
import play.*;
import play.mvc.*;
import org.codehaus.jackson.JsonNode; //Fixing "error: cannot find symbol" for JsonNode
// Testing JSON
#BodyParser.Of(BodyParser.Json.class) //Or you can import play.mvc.BodyParser.Json
public static Result sayHello() {
JsonNode json = request().body().asJson();
String name = json.findPath("name").getTextValue();
if(name==null) {
return badRequest("Missing parameter [name]");
} else {
return ok("Hello " + name);
}
}
AFAIK, the code you are using has not reached any official Play version (neither 2.0 or 2.0.1) according to this: https://github.com/playframework/Play20/pull/212
Instead, you can do this (not tested):
if(request().getHeader(play.mvc.Http.HeaderNames.ACCEPT).equalsIgnoreCase("application/json")) {
Did you try checking out the documentation for it?
Serving a JSON response looks like:
#BodyParser.Of(Json.class)
public static index sayHello() {
JsonNode json = request().body().asJson();
ObjectNode result = Json.newObject();
String name = json.findPath("name").getTextValue();
if(name == null) {
result.put("status", "KO");
result.put("message", "Missing parameter [name]");
return badRequest(result);
} else {
result.put("status", "OK");
result.put("message", "Hello " + name);
return ok(result);
}
}
You have imported play.libs.Json and then use the BodyParser.Of annotation with this Json.class.
The above annotation expects a class which extends a play.mvc.BodyParser. So simply replace #BodyParser.Of(Json.class) by #BodyParser.Of(BodyParser.Json.class).