HTML Diff tool API [closed] - html

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 4 years ago.
Improve this question
I'm looking for an api that will visually show html difference for both structure, characters/words, and style. This tool must also support double byte characters and be flexible enough for me to add it to my existing website to show the results of the comparison easily. I'm currently using the Component Software COM implementation which doesn't support double byte characters and hasn't been updated in about six years.

The only two tools I found that can do something like that are http://changedetection.com and http://imnosy.com. Both offer you to specify a url and watch them for changes.

This is what I used:
[http://code.google.com/p/google-diff-match-patch/][1]
I had to write my own methods to do the compare but after a little work it looks fine. This implementation compares test as passed in so it works fine if you are just comparing 2 text strings. My diff_prettyHtml call was changed to:
public string diff_prettyHtml(List<Diff> diffs)
{
StringBuilder html = new StringBuilder();
foreach (Diff aDiff in diffs)
{
string text = aDiff.text.Replace("&", "&").Replace("<", "<")
.Replace(">", ">").Replace("\n", "<br>");
switch (aDiff.operation)
{
case Operation.INSERT:
html.Append("<ins class='diff'>").Append(text)
.Append("</ins>");
break;
case Operation.DELETE:
html.Append("<del class='diff'>").Append(text)
.Append("</del>");
break;
case Operation.EQUAL:
html.Append("<span>").Append(text).Append("</span>");
break;
}
}
return html.ToString();
}
Now if you want to do a compare preview of 2 html strings this is a little different. This is what I did:
DiffMatchPatch.diff_match_patch diff = new DiffMatchPatch.diff_match_patch();
List<DiffMatchPatch.Diff> differences = diff.diff_main(oldHtml,
newHtml);
return diff.diff_previewHtml(differences);
public string diff_previewHtml(List<Diff> diffs) {
StringBuilder html = new StringBuilder();
foreach (Diff aDiff in diffs) {
string text = aDiff.text;
switch (aDiff.operation) {
case Operation.INSERT:
html.Append("<ins class='diff'>").Append(text)
.Append("</ins>");
break;
case Operation.DELETE:
html.Append("<del class='diff'>").Append(text)
.Append("</del>");
break;
case Operation.EQUAL:
html.Append(text);
break;
}
}
return html.ToString();
}
The unicode class is as follows:
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
namespace HtmlCompare
{
class Unicoder
{
private Hashtable _htmlHash = new Hashtable();
private const string _htmlPattern = #"<(S*?)[^>]*>.*?|<.*?\/>";
private List<string> _blockElements = "img,br".Split(',').ToList<string>();
private int _currentHash = 44032;
public string pushHash(string tag)
{
if (_htmlHash[tag] == null)
{
//_htmlHash[tag] = char.Parse("\\u" + Convert.ToString(_currentHash,16));
_htmlHash[tag] = char.ConvertFromUtf32(_currentHash);
_currentHash++;
}
return _htmlHash[tag].ToString();
}
private string tagMatch(Match tag)
{
return pushHash(tag.Value);
}
public string html2plain(string html)
{
MatchEvaluator tagEvaluator = new MatchEvaluator(tagMatch);
return Regex.Replace(html, _htmlPattern, tagEvaluator, RegexOptions.IgnoreCase | RegexOptions.Multiline);
}
private string ProcessDiffTag(string tagStart, string tagEnd, string contents)
{
ArrayList diffTagParts = new ArrayList();
MatchCollection matches = Regex.Matches(contents,
_htmlPattern,
RegexOptions.IgnoreCase | RegexOptions.Multiline);
if (matches.Count > 0)
{
int contentsStringIndex = 0;
int contentsStringEndIndex = 0;
int lastContentStringIndex = 0;
bool lastTag = false;
TagDefinition definition;
foreach (Match currentMatch in matches)
{
contentsStringIndex = currentMatch.Index;
contentsStringEndIndex = contentsStringIndex + currentMatch.Length;
lastTag = (currentMatch == matches[matches.Count - 1]);
// did we miss text that isn't a tag?
if (contentsStringIndex > lastContentStringIndex)
{
definition = new TagDefinition();
definition.Tag = false;
definition.Text = contents.Substring(lastContentStringIndex, contentsStringIndex - lastContentStringIndex);
AddTagDefinition(diffTagParts, definition);
}
else if (lastTag && contents.Length > contentsStringEndIndex) // something after the last tag?
{
definition = new TagDefinition();
definition.Tag = false;
definition.Text = contents.Substring(contentsStringEndIndex, contents.Length - contentsStringEndIndex);
AddTagDefinition(diffTagParts, definition);
}
// work on current tag
definition = new TagDefinition();
definition.Tag = true;
definition.OpeningTag = !IsClosingTag(currentMatch.Value);
definition.TagType = GetTagType(currentMatch.Value);
definition.Text = currentMatch.Value;
AddTagDefinition(diffTagParts, definition);
lastContentStringIndex = contentsStringEndIndex;
}
return GoThroughDiffParts(diffTagParts,
tagStart,
tagEnd);
}
else
return string.Concat(tagStart, contents, tagEnd);
}
private string GetTagType(string tag)
{
int startIndex = 1; // skip <
if (tag.StartsWith("</"))
startIndex = 2; // skip </
int endIndex = tag.IndexOf(" ");
if (endIndex == -1)
endIndex = tag.IndexOf(">");
return tag.Substring(startIndex, endIndex - startIndex);
}
private string GoThroughDiffParts(ArrayList parts, string startTag, string endTag)
{
IEnumerator enumerator = parts.GetEnumerator();
StringBuilder before = new StringBuilder(string.Empty);
StringBuilder middle = new StringBuilder(string.Empty);
StringBuilder after = new StringBuilder(string.Empty);
TagDefinition definition;
while (enumerator.MoveNext())
{
definition = (TagDefinition)enumerator.Current;
if (!definition.Used) // have we already used this part?
{
definition.Used = true;
if (_blockElements.Contains(definition.TagType))
middle.Append(definition.Text);
else if (definition.MatchingIndex == -1) // no matching tag
{
if (definition.Tag) // html tag?
{
if (definition.OpeningTag)
before.Append(definition.Text);
else
after.Append(definition.Text);
}
else
middle.Append(definition.Text);
}
else
{
if (!definition.Tag) // text and has a matching tag
{
TagDefinition matchingTag = (TagDefinition)parts[definition.MatchingIndex];
if (matchingTag.OpeningTag)
matchingTag.Text += definition.Text;
else
matchingTag.Text = string.Concat(definition.Text, matchingTag.Text);
definition.Used = true;
}
else
middle.Append(definition.Text);
}
}
}
bool includeDiffTag = true;
if (string.IsNullOrEmpty(middle.ToString()))
includeDiffTag = false; // we don't want the ins/del tag around nothing
else if (string.IsNullOrWhiteSpace(middle.ToString())) // spacing should be kept
middle = new StringBuilder(" " + middle.Replace("\n", "<br />"));
if(includeDiffTag)
middle.Insert(0, startTag); // <ins>[middle]
middle.Insert(0, before); // [before]<ins>[middle]
if (includeDiffTag)
middle.Append(endTag); // [before]<ins>[middle]</ins>
middle.Append(after); // [before]<ins>[middle]</ins>[end]
return middle.ToString();
}
private string DiffTagMatch(Match tag)
{
string tagStart = tag.Groups[1].Value;
string tagEnd = tag.Groups[5].Value;
string contents = tag.Groups[4].Value;
if (string.IsNullOrEmpty(contents))
return string.Empty; // we don't want the ins/del tag around nothing
else if (string.IsNullOrWhiteSpace(contents)) // spacing should be kept
return string.Concat(tagStart, " ", contents.Replace("\n", "<br />"), tagEnd);
else
return ProcessDiffTag(tagStart,
tagEnd,
contents);
}
private bool IsClosingTag(string tag)
{
return tag.Contains("</") && !tag.ToLower().Contains("<img") && !tag.ToLower().Contains("<br");
}
public string CleanUpMisplacedDiffTags(string html)
{
return Regex.Replace(html, #"(\<((ins|del).*?)\>)(.*?)(\<\/((ins|del).*?)\>)", DiffTagMatch, RegexOptions.IgnoreCase | RegexOptions.Multiline);
}
public string plain2html(string plain)
{
IDictionaryEnumerator enumerator = _htmlHash.GetEnumerator();
while (enumerator.MoveNext())
{
plain = Regex.Replace(plain,
_htmlHash[enumerator.Key].ToString(),
enumerator.Key.ToString(),
RegexOptions.IgnoreCase | RegexOptions.Multiline);
}
return CleanUpMisplacedDiffTags(plain);
}
private void AddTagDefinition(ArrayList list, TagDefinition tag)
{
IEnumerator enumerator = list.GetEnumerator();
TagDefinition currentDefinition;
int index = 0;
int insertingIndex = list.Count;
while (enumerator.MoveNext())
{
currentDefinition = (TagDefinition)enumerator.Current;
//if (!tag.OpeningTag && currentDefinition.MatchingIndex == -1)
// currentDefinition.MatchingIndex = insertingIndex;
if (tag.MatchingIndex == -1 && // matching tag not found yet
(currentDefinition.OpeningTag && !tag.OpeningTag) && // opening & closing
currentDefinition.TagType == currentDefinition.TagType) // same tag type
{
tag.MatchingIndex = index;
currentDefinition.MatchingIndex = insertingIndex;
}
}
list.Add(tag);
}
private class TagDefinition
{
public bool Tag { get; set; }
public string TagType { get; set; }
public string Text { get; set; }
public int MatchingIndex { get; set; }
public bool OpeningTag { get; set; }
public bool Used { get; set; }
public TagDefinition()
{
this.Tag = false;
this.Text = string.Empty;
this.TagType = string.Empty;
this.MatchingIndex = -1;
this.OpeningTag = false;
this.Used = false;
}
}
}
}

Related

Serializing and deserializing objects

I am trying to serialize and deserialize my objects to save and load data.
I thought I was smart and introduced Attributes:
[ExposedProperty]
public float Width { get; set; }
[ExposedProperty]
public Color HoverColor { get; set; }
I have a PropertyData class:
[System.Serializable]
public class PropertyData
{
public string Name;
public string Type;
public object Value;
public override string ToString()
{
return "PropertyData ( Name = " + Name + ", Type = " + Type + ", Value = " + Value + ")";
}
}
So instead of writing an ObjectData class for every Object class I have that gets serialized into JSON, I though I'd write a Serializable class that does:
public List<PropertyData> SerializeProperties()
{
var list = new List<PropertyData>();
var type = this.GetType();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)
.Where(p => p.GetCustomAttributes(typeof(ExposedPropertyAttribute), false).Length > 0)
.ToArray();//
for (int i = 0; i < properties.Length; i++)
{
var property = properties[i];
var data = new PropertyData();
data.Name = property.Name;
data.Type = property.PropertyType.Name;
data.Value = property.GetValue(this);
list.Add(data);
}
return list;
}
and also to deserialize:
protected void DeserializePropertyData(PropertyData data)
{
var p = this.GetType().GetProperty(data.Name);
if (p == null)
{
Debug.LogError("Item " + this + " does not have a property with name '" + data.Name + "'");
return;
}
var type = p.PropertyType;
try
{
//TODO do some magic here to deserialize any of the values.
TypeConverter typeConverter = TypeDescriptor.GetConverter(type);
object propValue = typeConverter.ConvertFromString(data.Value);
p.SetValue(this, propValue);
}
catch(FormatException fe)
{
Debug.Log($"Serializable, there was a format exception on property {data.Name} and value {data.Value} for type {type}");
}
catch(NotSupportedException nse)
{
Debug.Log($"Serializable, there was a not supported exception on property {data.Name} and value {data.Value} for type {type}");
}
finally
{
}
}
But as it turns out, I can't serialize or deserialize Color, or Vector3, or Quaternion, or whatever. It only works for bool, float, string, int...
Any ideas how to serialize/deserialize other objects properly?
Thanks for all the answers. I looked into the ISerializationSurrogate Interface and noticed that it can't be used for UnityEngine.Vector3 or UnityEngine.Color classes.
Then I looked into TypeConverter and also saw that you can't directly use it because you have to add the attribute [TypeConverter(typeof(CustomTypeConverter))] on top of the very class or struct you want to convert.
And at the end of the day, both TypeConverter and ISerializationSurrogate are simply pushing and parsing chars around to get the result. For a Vector3, you have to trim the "(", ")", split the string with a "," and perform float.Parse on every element of the split string array. For a Color, you have to trim "RGBA(" and ")", and do the exact same thing.
Deadline is tight, so I took the same principle and created a static class that does my converting:
public static class MyCustomTypeConverter
{
public static object ConvertFromString(string value, Type destinationType)
{
if (destinationType == typeof(string))
{
return value;
}
else if (destinationType == typeof(int))
{
return int.Parse(value);
}
else if (destinationType == typeof(float))
{
return float.Parse(value);
}
else if (destinationType == typeof(double))
{
return double.Parse(value);
}
else if (destinationType == typeof(long))
{
return long.Parse(value);
}
else if (destinationType == typeof(bool))
{
return bool.Parse(value);
}
else if (destinationType == typeof(Vector3))
{
var mid = value.Substring(1, value.Length - 2);
var values = mid.Split(",");
return new Vector3(
float.Parse(values[0], CultureInfo.InvariantCulture),
float.Parse(values[1], CultureInfo.InvariantCulture),
float.Parse(values[2], CultureInfo.InvariantCulture)
);
}
else if (destinationType == typeof(Vector4))
{
var mid = value.Substring(1, value.Length - 2);
var values = mid.Split(",");
return new Vector4(
float.Parse(values[0], CultureInfo.InvariantCulture),
float.Parse(values[1], CultureInfo.InvariantCulture),
float.Parse(values[2], CultureInfo.InvariantCulture),
float.Parse(values[3], CultureInfo.InvariantCulture)
);
}
else if (destinationType == typeof(Color))
{
var mid = value.Substring(5, value.Length - 6);
var values = mid.Split(",");
return new Color(
float.Parse(values[0], CultureInfo.InvariantCulture),
float.Parse(values[1], CultureInfo.InvariantCulture),
float.Parse(values[2], CultureInfo.InvariantCulture),
float.Parse(values[3], CultureInfo.InvariantCulture)
);
}
else if (destinationType == typeof(PrimitiveType))
{
var e = Enum.Parse<PrimitiveType>(value);
return e;
}
else if (destinationType == typeof(SupportedLightType))
{
var e = Enum.Parse<SupportedLightType>(value);
return e;
}
return null;
}
public static string ConvertToString(object value)
{
return value.ToString();
}
}
It works, does what I want, and I can extend it for further classes or structs. Quaternion or Bounds perhaps, but I don't need much more than that.
My Serialization class now does this:
public List<PropertyData> SerializeProperties()
{
var list = new List<PropertyData>();
var type = this.GetType();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty)
.Where(p => p.GetCustomAttributes(typeof(ExposedPropertyAttribute), false).Length > 0)
.ToArray();//
for (int i = 0; i < properties.Length; i++)
{
var property = properties[i];
var data = new PropertyData();
data.Name = property.Name;
data.Value = MyCustomTypeConverter.ConvertToString(property.GetValue(this));
list.Add(data);
}
return list;
}
and that:
protected void DeserializePropertyData(PropertyData data)
{
var p = this.GetType().GetProperty(data.Name);
if (p == null)
{
return;
}
var type = p.PropertyType;
object propValue = MyCustomTypeConverter.ConvertFromString(data.Value, type);
p.SetValue(this, propValue);
}

MySQL JSON_EXTRACT value of property based on criteria

Suppose a JSON column called blob in a MySQL 5.7 database table called "Thing" with the following content:
[
{
"id": 1,
"value": "blue"
},
{
"id": 2,
"value": "red"
}
]
Is it possible to select all records from Thing where the blob contains an object within the array where the id is some dynamic value and the value is also some dynamic value.
E.g. "give me all Things where the blob contains an object whose id is 2 and value is 'red'"
Not sure how to form the WHERE clause below:
SET #id = 2;
SET #value1 = 'red';
SET #value2 = 'blue';
-- with equals?
SELECT *
FROM Thing
WHERE JSON_EXTRACT(blob, '$[*].id ... equals #id ... and .value') = #value1;
-- with an IN clause?
SELECT *
FROM Thing
WHERE JSON_EXTRACT(blob, '$[*].id ... equals #id ... and .value') IN (#value1, #value2);
They said it couldn't be done. They told me I was a fool. Lo and behold, I have done it!
Here are some helper functions to teach Hibernate how to perform JSON functions against a MySQL 5.7 backend, and an example use case. It's confusing, for sure, but it works.
Context
This contrived example is of a Person entity which can have many BioDetails, which is a question/answer type (but is more involved than that). The example within below essentially is searching within two JSON payloads, grabbing JSON values from one to build JSON paths within which to search in the other. In the end, you can pass in a complex structure of AND'd or OR'd criteria, which will be applied against a JSON blob and return only the resulting rows which match.
E.g. give my all Person entities where their age is > 30 and their favorite color is blue or orange. Given that those key/value pairs are stored in a JSON blob, you can find those matched using the example code below.
JSON-search classes and example repo
Classes below use Lombok for brevity.
Classes to allow specification of search criteria
SearchCriteriaContainer
#Data
#EqualsAndHashCode
public class SearchCriteriaContainer
{
private List<SearchCriterion> criteria;
private boolean and;
}
SearchCriterion
#Data
#EqualsAndHashCode(callSuper = true)
public class SearchCriterion extends SearchCriteriaContainer
{
private String field;
private List<String> values;
private SearchOperator operator;
private boolean not = false;
}
SearchOperator
#RequiredArgsConstructor
public enum SearchOperator
{
EQUAL("="),
LESS_THAN("<"),
LESS_THAN_OR_EQUAL("<="),
GREATER_THAN(">"),
GREATER_THAN_OR_EQUAL(">="),
LIKE("like"),
IN("in"),
IS_NULL("is null");
private final String value;
#JsonCreator
public static SearchOperator fromValue(#NotBlank String value)
{
return Stream
.of(SearchOperator.values())
.filter(o -> o.getValue().equals(value))
.findFirst()
.orElseThrow(() ->
{
String message = String.format("Could not find %s with value: %s", SearchOperator.class.getName(), value);
return new IllegalArgumentException(message);
});
}
#JsonValue
public String getValue()
{
return this.value;
}
#Override
public String toString()
{
return value;
}
}
Helper class which is used to call JSON functions
#RequiredArgsConstructor
public class CriteriaBuilderHelper
{
private final CriteriaBuilder criteriaBuilder;
public Expression<String> concat(Expression<?>... values)
{
return criteriaBuilder.function("CONCAT", String.class, values);
}
public Expression<String> substringIndex(Expression<?> value, String delimiter, int count)
{
return substringIndex(value, criteriaBuilder.literal(delimiter), criteriaBuilder.literal(count));
}
public Expression<String> substringIndex(Expression<?> value, Expression<String> delimiter, Expression<Integer> count)
{
return criteriaBuilder.function("SUBSTRING_INDEX", String.class, value, delimiter, count);
}
public Expression<String> jsonUnquote(Expression<?> jsonValue)
{
return criteriaBuilder.function("JSON_UNQUOTE", String.class, jsonValue);
}
public Expression<String> jsonExtract(Expression<?> jsonDoc, Expression<?> path)
{
return criteriaBuilder.function("JSON_EXTRACT", String.class, jsonDoc, path);
}
public Expression<String> jsonSearchOne(Expression<?> jsonDoc, Expression<?> value, Expression<?>... paths)
{
return jsonSearch(jsonDoc, "one", value, paths);
}
public Expression<String> jsonSearch(Expression<?> jsonDoc, Expression<?> value, Expression<?>... paths)
{
return jsonSearch(jsonDoc, "all", value, paths);
}
public Expression<String> jsonSearch(Expression<?> jsonDoc, String oneOrAll, Expression<?> value, Expression<?>... paths)
{
if (!"one".equals(oneOrAll) && !"all".equals(oneOrAll))
{
throw new RuntimeException("Parameter 'oneOrAll' must be 'one' or 'all', not: " + oneOrAll);
}
else
{
final var expressions = new ArrayList<>(List.of(
jsonDoc,
criteriaBuilder.literal(oneOrAll),
value,
criteriaBuilder.nullLiteral(String.class)));
if (paths != null)
{
expressions.addAll(Arrays.asList(paths));
}
return criteriaBuilder.function("JSON_SEARCH", String.class, expressions.toArray(Expression[]::new));
}
}
}
Utility to turn SearchCriteria into MySQL JSON function calls
SearchHelper
public class SearchHelper
{
private static final Pattern pathSeparatorPattern = Pattern.compile("\\.");
public static String getKeyPart(String key)
{
return pathSeparatorPattern.split(key)[0];
}
public static String getPathPart(String key)
{
final var parts = pathSeparatorPattern.split(key);
final var path = new StringBuilder();
for (var i = 1; i < parts.length; i++)
{
if (i > 1)
{
path.append(".");
}
path.append(parts[i]);
}
return path.toString();
}
public static Optional<Predicate> getCriteriaPredicate(SearchCriteriaContainer container, CriteriaBuilder cb, Path<String> bioDetailJson, Path<String> personJson)
{
final var predicates = new ArrayList<Predicate>();
if (container != null && container.getCriteria() != null && container.getCriteria().size() > 0)
{
final var h = new CriteriaBuilderHelper(cb);
container.getCriteria().forEach(ac ->
{
final var groupingOnly = ac.getField() == null && ac.getOperator() == null;
// a criterion can be used for grouping other criterion, and might not have a field/operator/value
if (!groupingOnly)
{
final var key = getKeyPart(ac.getField());
final var path = getPathPart(ac.getField());
final var bioDetailQuestionKeyPathEx = h.jsonUnquote(h.jsonSearchOne(bioDetailJson, cb.literal(key), cb.literal("$[*].key")));
final var bioDetailQuestionIdPathEx = h.concat(h.substringIndex(bioDetailQuestionKeyPathEx, ".", 1), cb.literal(".id"));
final var questionIdEx = h.jsonUnquote(h.jsonExtract(bioDetailJson, bioDetailQuestionIdPathEx));
final var answerPathEx = h.substringIndex(h.jsonUnquote(h.jsonSearchOne(personJson, questionIdEx, cb.literal("$[*].questionId"))), ".", 1);
final var answerValuePathEx = h.concat(answerPathEx, cb.literal("." + path));
final var answerValueEx = h.jsonUnquote(h.jsonExtract(personJson, answerValuePathEx));
switch (ac.getOperator())
{
case IN:
{
final var inEx = cb.in(answerValueEx);
if (ac.getValues() == null || ac.getValues().size() == 0)
{
throw new RuntimeException("No values provided for 'IN' criteria for field: " + ac.getField());
}
else
{
ac.getValues().forEach(inEx::value);
}
predicates.add(inEx);
break;
}
case IS_NULL:
{
predicates.add(cb.isNull(answerValueEx));
break;
}
default:
{
if (ac.getValues() == null || ac.getValues().size() == 0)
{
throw new RuntimeException("No values provided for '" + ac.getOperator() + "' criteria for field: " + ac.getField());
}
else
{
ac.getValues().forEach(value ->
{
final var valueEx = cb.literal(value);
switch (ac.getOperator())
{
case EQUAL:
{
predicates.add(cb.equal(answerValueEx, valueEx));
break;
}
case LESS_THAN:
{
predicates.add(cb.lessThan(answerValueEx, valueEx));
break;
}
case LESS_THAN_OR_EQUAL:
{
predicates.add(cb.lessThanOrEqualTo(answerValueEx, valueEx));
break;
}
case GREATER_THAN:
{
predicates.add(cb.greaterThan(answerValueEx, valueEx));
break;
}
case GREATER_THAN_OR_EQUAL:
{
predicates.add(cb.greaterThanOrEqualTo(answerValueEx, valueEx));
break;
}
case LIKE:
{
predicates.add(cb.like(answerValueEx, valueEx));
break;
}
default:
throw new RuntimeException("Unsupported operator during snapshot search: " + ac.getOperator());
}
});
}
}
}
}
// iterate nested criteria
getAnswerCriteriaPredicate(ac, cb, bioDetailJson, personJson).ifPresent(predicates::add);
});
return Optional.of(container.isAnd()
? cb.and(predicates.toArray(Predicate[]::new))
: cb.or(predicates.toArray(Predicate[]::new)));
}
else
{
return Optional.empty();
}
}
}
Example JPA Specification repository / search method
ExampleRepository
#Repository
public interface PersonRepository extends JpaSpecificationExecutor<Person>
{
default Page<Person> search(PersonSearchDirective directive, Pageable pageable)
{
return findAll((person, query, cb) ->
{
final var bioDetail = person.join(Person_.bioDetail);
final var bioDetailJson = bioDetail.get(BioDetailEntity_.bioDetailJson);
final var personJson = person.get(Person_.personJson);
final var predicates = new ArrayList<>();
SearchHelper
.getCriteriaPredicate(directive.getSearchCriteria(), cb, bioDetailJson, personJson)
.ifPresent(predicates::add);
return cb.and(predicates.toArray(Predicate[]::new));
}, pageable);
}
}

How to split pdf file by book marks using itext 7 , if pdf contains "Duplicate Bookmarks"

I am trying to split pdf by its bookmarks using itext7.
Problem : if Pdf is having same bookmark in other place in the outline tree , it is over ridding and unable to split.
Sample code to reproduce the problem:
public void walkOutlines(PdfOutline outline, Map<String, PdfObject> names, PdfDocument pdfDocument,List<String>titles,List<Integer>pageNum) { //----------loop traversing all paths
for (PdfOutline child : outline.getAllChildren()){
if(child.getDestination() != null) {
prepareIndexFile(child,names,pdfDocument,titles,pageNum,list);
}
}
}
//------------Getting pageNumbers from outlines
public void prepareIndexFile(PdfOutline outline, Map<String, PdfObject> names, PdfDocument pdfDocument,List<String>titles,List<Integer>pageNum) {
String title = outline.getTitle();
PdfDestination pdfDestination = outline.getDestination();
String pdfStr = ((PdfString)pdfDestination.getPdfObject()).toUnicodeString();
PdfArray array = (PdfArray) names.get(pdfStr);
PdfObject pdfObj = array != null ? array.get(0) : null;
Integer pageNumber = pdfDocument.getPageNumber((PdfDictionary)pdfObj);
titles.add(title);
pageNum.add(pageNumber);
if(outline.getAllChildren().size() > 0) {
for (PdfOutline child : outline.getAllChildren()){
prepareIndexFile(child,names,pdfDocument,titles,pageNum);
}
}
}
public boolean splitPdf(String inputFile, final String outputFolder) {
boolean splitSuccess = true;
PdfDocument pdfDoc = null;
try {
PdfReader pdfReaderNew = new PdfReader(inputFile);
pdfDoc = new PdfDocument(pdfReaderNew);
final List<String> titles = new ArrayList<String>();
List<Integer> pageNum = new ArrayList<Integer>();
PdfNameTree destsTree = pdfDoc.getCatalog().getNameTree(PdfName.Dests);
Map<String, PdfObject> names = destsTree.getNames();//--------------------------------------Core logic for getting names
PdfOutline root = pdfDoc.getOutlines(false);//--------------------------------------Core logic for getting outlines
walkOutlines(root,names, pdfDoc, titles, pageNum,content); //------Logic to get bookmarks and pageNumbers
if (titles == null || titles.size()==0) {
splitSuccess = false;
}else { //------Proceed if it has bookmarks
for(int i=0;i<titles.size();i++) {
String title = titles.get(i);
String startPageNmStr =""+pageNum.get(i);
int startPage = Integer.parseInt(startPageNmStr);
int endPage = startPage;
if(i == titles.size() - 1) {
endPage = pdfDoc.getNumberOfPages();
}else {
int nextPage = pageNum.get(i+1);
if(nextPage > startPage) {
endPage = nextPage - 1;
}else {
endPage = nextPage;
}
}
String outFileName = outputFolder + File.separator + getFileName(title) + ".pdf";
PdfWriter pdfWriter = new PdfWriter(outFileName);
PdfDocument newDocument = new PdfDocument(pdfWriter, new DocumentProperties().setEventCountingMetaInfo(null));
pdfDoc.copyPagesTo(startPage, endPage, newDocument);
newDocument.close();
pdfWriter.close();
}
}
}catch(Exception e){
//---log
}
}
Found root cause: In PdfNameTree items.put(name.toUnicodeString(), names.get(k));
How to over come this issue?
Thanks in advance
This part of the code:
PdfDestination pdfDestination = outline.getDestination();
String pdfStr = ((PdfString)pdfDestination.getPdfObject()).toUnicodeString();
PdfArray array = (PdfArray) names.get(pdfStr);
PdfObject pdfObj = array != null ? array.get(0) : null;
Integer pageNumber = pdfDocument.getPageNumber((PdfDictionary)pdfObj);
Does not take into account the case that the destination can be non-named and refer to a page explicitly.
So the code needs to be adapted into the following code:
PdfDestination pdfDestination = outline.getDestination();
PdfObject pdfObj = null;
if (pdfDestination.getPdfObject().isString()) {
String pdfStr = ((PdfString) pdfDestination.getPdfObject()).toUnicodeString();
PdfArray array = (PdfArray) names.get(pdfStr);
if (array != null) {
pdfObj = array.get(0);
}
} else if (pdfDestination.getPdfObject().isArray() && ((PdfArray)pdfDestination.getPdfObject()).get(0).isDictionary()) {
pdfObj = ((PdfArray)pdfDestination.getPdfObject()).get(0);
}
Integer pageNumber = pdfDocument.getPageNumber((PdfDictionary)pdfObj);
Additionally, if you want to obtain the full title names including the parent chain, you need to replace String title = outline.getTitle(); with the following piece of code:
String title = outline.getTitle();
PdfOutline parentChain = outline.getParent();
while (parentChain != null) {
title = parentChain.getTitle() + "." + title;
parentChain = parentChain.getParent();
}
As a result, I got 6 files in the output directory, with 5 files of 1 page each and one file of 4 pages.
Complete code:
public void walkOutlines(PdfOutline outline, Map<String, PdfObject> names, PdfDocument pdfDocument,
java.util.List<String>titles,java.util.List<Integer>pageNum) { //----------loop traversing all paths
for (PdfOutline child : outline.getAllChildren()){
if(child.getDestination() != null) {
prepareIndexFile(child,names,pdfDocument,titles,pageNum);
}
}
}
//------------Getting pageNumbers from outlines
public void prepareIndexFile(PdfOutline outline, Map<String, PdfObject> names, PdfDocument pdfDocument,
java.util.List<String>titles,java.util.List<Integer>pageNum) {
String title = outline.getTitle();
PdfOutline parentChain = outline.getParent();
while (parentChain != null) {
title = parentChain.getTitle() + "." + title;
parentChain = parentChain.getParent();
}
PdfDestination pdfDestination = outline.getDestination();
PdfObject pdfObj = null;
if (pdfDestination.getPdfObject().isString()) {
String pdfStr = ((PdfString) pdfDestination.getPdfObject()).toUnicodeString();
PdfArray array = (PdfArray) names.get(pdfStr);
if (array != null) {
pdfObj = array.get(0);
}
} else if (pdfDestination.getPdfObject().isArray() && ((PdfArray)pdfDestination.getPdfObject()).get(0).isDictionary()) {
pdfObj = ((PdfArray)pdfDestination.getPdfObject()).get(0);
}
Integer pageNumber = pdfDocument.getPageNumber((PdfDictionary)pdfObj);
titles.add(title);
pageNum.add(pageNumber);
if(outline.getAllChildren().size() > 0) {
for (PdfOutline child : outline.getAllChildren()){
prepareIndexFile(child,names,pdfDocument,titles,pageNum);
}
}
}
public void splitPdf(String inputFile, final String outputFolder) {
boolean splitSuccess = true;
PdfDocument pdfDoc = null;
try {
PdfReader pdfReaderNew = new PdfReader(inputFile);
pdfDoc = new PdfDocument(pdfReaderNew);
final java.util.List<String> titles = new ArrayList<String>();
java.util.List<Integer> pageNum = new ArrayList<Integer>();
PdfNameTree destsTree = pdfDoc.getCatalog().getNameTree(PdfName.Dests);
Map<String, PdfObject> names = destsTree.getNames();//--------------------------------------Core logic for getting names
PdfOutline root = pdfDoc.getOutlines(false);//--------------------------------------Core logic for getting outlines
walkOutlines(root,names, pdfDoc, titles, pageNum); //------Logic to get bookmarks and pageNumbers
if (titles == null || titles.size()==0) {
splitSuccess = false;
}else { //------Proceed if it has bookmarks
for(int i=0;i<titles.size();i++) {
String title = titles.get(i);
String startPageNmStr =""+pageNum.get(i);
int startPage = Integer.parseInt(startPageNmStr);
int endPage = startPage;
if(i == titles.size() - 1) {
endPage = pdfDoc.getNumberOfPages();
}else {
int nextPage = pageNum.get(i+1);
if(nextPage > startPage) {
endPage = nextPage - 1;
}else {
endPage = nextPage;
}
}
String outFileName = outputFolder + File.separator + title + ".pdf";
PdfWriter pdfWriter = new PdfWriter(outFileName);
PdfDocument newDocument = new PdfDocument(pdfWriter, new DocumentProperties().setEventCountingMetaInfo(null));
pdfDoc.copyPagesTo(startPage, endPage, newDocument);
newDocument.close();
pdfWriter.close();
}
}
}catch(IOException e){
System.out.println(e);
}
}

vala: Serializing object property with Json.gobject_serialize?

I need to save an object's state into a file and retrieve it later. I found JSON serialization would help and found this method Json.gobject_serialize. Using this method, I can successfully serialize objects containing string properties. But what should I do, if the object A consists of another object (say B) within it and I need to serialize object A.
EDIT
What should I do if the object A consists of array (say B) of objects?
I created a small test program for this purpose and I failed in that try. I cannot find any detailed documentation about JSON Serialization for vala.
public class Foo : Object {
public int iFoo {get; set;}
public string sFoo {get; set;}
Bar[] _bar = {};
public Bar[] bar {get {return _bar;} set{_bar = value;}}
public class Bar : Object {
public int iBar {get; set;}
public string sBar {get; set;}
construct {
iBar = 02;
sBar = "OutOfRange";
}
}
construct {
_bar += new Bar();
iFoo = 74;
sFoo = "GIrafee";
}
public static int main () {
Json.Node root = Json.gobject_serialize (new Foo());
Json.Generator generator = new Json.Generator ();
generator.set_root (root);
stdout.printf(generator.to_data (null) + "\n");
return 0;
}
}
Serialization with JSON-GLib is recursive for properties containing complex types.
If the property of a GObject contains another GObject, json_gobject_serialize() will recursively call json_gobject_serialize() on the instance stored inside the property — or serialize the null if the property is unset.
I've implemented a object to support Json.Serializable interface as follow:
public class DbObject : GLib.Object, Json.Serializable
{
public Json.Object? meta { get; construct set; default = null; }
public VersionSync version { get; set; default = VersionSync.UNKNOWN; }
public virtual Value get_property (ParamSpec pspec)
{
Value prop_value = GLib.Value(pspec.value_type);
(this as GLib.Object).get_property(pspec.name, ref prop_value);
stdout.printf ("%s --> %s\n", prop_value.type_name(), prop_value.strdup_contents());
return prop_value;
}
public virtual void set_property (ParamSpec pspec, Value value)
{
(this as GLib.Object).set_property (pspec.name, value);
}
public unowned ParamSpec? find_property (string name)
{
return ((ObjectClass) get_type ().class_ref ()).find_property (name);
}
public virtual Json.Node serialize_property (string property_name, Value #value, ParamSpec pspec)
{
if (#value.type ().is_a (typeof (Json.Object)))
{
var obj = #value as Json.Object;
if (obj != null)
{
var node = new Json.Node (NodeType.OBJECT);
node.set_object (obj);
return node;
}
}
else if (#value.type ().is_a (typeof (Gee.ArrayList)))
{
unowned Gee.ArrayList<GLib.Object> list_value = #value as Gee.ArrayList<GLib.Object>;
if (list_value != null || property_name == "data")
{
var array = new Json.Array.sized (list_value.size);
foreach (var item in list_value)
{
array.add_element (gobject_serialize (item));
}
var node = new Json.Node (NodeType.ARRAY);
node.set_array (array);
return node;
}
}
else if (#value.type ().is_a (typeof (GLib.Array)))
{
unowned GLib.Array<GLib.Object> array_value = #value as GLib.Array<GLib.Object>;
if (array_value != null || property_name == "data")
{
var array = new Json.Array.sized (array_value.length);
for (int i = 0; i < array_value.length; i++) {
array.add_element (gobject_serialize (array_value.index(i)));
}
var node = new Json.Node (NodeType.ARRAY);
node.set_array (array);
return node;
}
}
else if (#value.type ().is_a (typeof (HashTable)))
{
var obj = new Json.Object ();
var ht_string = #value as HashTable<string, string>;
if (ht_string != null)
{
ht_string.foreach ((k, v) => {
obj.set_string_member (k, v);
});
var node = new Json.Node (NodeType.OBJECT);
node.set_object (obj);
return node;
} else {
var ht_object = #value as HashTable<string, GLib.Object>;
if (ht_object != null)
{
ht_object.foreach ((k, v) => {
obj.set_member (k, gobject_serialize (v));
});
var node = new Json.Node (NodeType.OBJECT);
node.set_object (obj);
return node;
}
}
}
return default_serialize_property (property_name, #value, pspec);
}
public virtual bool deserialize_property (string property_name, out Value #value, ParamSpec pspec, Json.Node property_node)
{
return default_deserialize_property (property_name, out #value, pspec, property_node);
}
}

JSP tag library to display MySQL rollup query with grouping and subtotals

I need to display several tables as HTML, using JSP, coming from MySQL GROUP BY a,b,c WITH ROLLUP queries. I'm looking for a good tag library to achieve this. I have found DisplayTag, but it'was last updated in 2008. And I would prefer using the subtotals calculated by MySQL, which seems to be tricky with DisplayTag.
MySQL does subtotals by adding extra rows to the resultset with the group field set to NULL.
Is there a better alternative? Printing the table is important, paging and sorting would be nice but I can live without them. No editing of any kind.
I wrote my own quick-and-dirty tag. Note that it expects the rollup data structure returned by MySQL, haven't tested it with anything else.
Usage example:
<xxx:rollupTable cssClass="data" data="${data}">
<xxx:rollupColumn title="Person" align="left" group="true" fieldName="personName" groupFieldName="personId" tooltipLink="person"/>
<xxx:rollupColumn title="City" align="left" group="true" fieldName="cityName" groupFieldName="cityId" tooltipLink="city"/>
<xxx:rollupColumn title="Price" align="right" format="#,##0.000" fieldName="price"/>
<xxx:rollupColumn title="Amount" align="right" format="#,##0" fieldName="amount"/>
</xxx:rollupTable>
The column tag does not much but adds the column definition to the table tag for later use.
package xxx.tags;
import...
public class RollupTableColumnTag extends SimpleTagSupport {
private String title;
private boolean group = false;
private boolean sum = false;
private String fieldName; // field name to output
private String groupFieldName; // field name to test for rollup level changes
private String align;
private String format;
private String tooltipLink;
private DecimalFormat formatter;
public void doTag() throws IOException, JspTagException {
RollupTableTag parent = (RollupTableTag)findAncestorWithClass(this, RollupTableTag.class);
if (parent == null) {
throw new JspTagException("Parent tag not found.");
}
parent.addColumnDefinition(this);
}
public void setFormat(String format) {
formatter = new DecimalFormat(format);
this.format = format;
}
public DecimalFormat getFormatter() {
return formatter;
}
// other getters and setters are standard, excluded
}
The table tag does the actual hard work:
package xxx.tags;
import ...
public class RollupTableTag extends BodyTagSupport {
protected String cssClass;
protected List<Map> data;
protected List<RollupTableColumnTag> columns;
protected List<Integer> groups;
public void setCssClass(String cssClass) {
this.cssClass = cssClass;
}
public void setData(List data) {
this.data = (List<Map>)data;
}
public int doStartTag() throws JspException {
columns = new ArrayList<RollupTableColumnTag>();
groups = new ArrayList<Integer>();
return EVAL_BODY_BUFFERED;
}
public int doEndTag() throws JspException {
try {
JspWriter writer = pageContext.getOut();
if (data.size() == 0) {
writer.println("<P>No data.</P>");
return EVAL_PAGE;
}
int nLevels = groups.size();
int nNormalRowCount = 0;
boolean[] bStartGroup = new boolean[nLevels];
String[] sSummaryTitle = new String[nLevels];
for (int i=0;i<nLevels;i++) {
bStartGroup[i] = true;
}
writer.println("<TABLE class=\"" + cssClass + "\">");
writer.println("<THEAD><TR>");
for (RollupTableColumnTag column : columns) {
writer.print("<TH");
if (column.getAlign() != null) {
writer.print(" align=\"" + column.getAlign() + "\"");
}
writer.print(">" + column.getTitle() + "</TH>");
}
writer.println("</TR></THEAD>");
writer.println("<TBODY>");
for (Map dataRow : data) {
StringBuffer out = new StringBuffer();
out.append("<TR>");
// grouping columns always come first
String cellClass = null;
for (int i=0;i<nLevels-1;i++) {
if (bStartGroup[i]) {
Object dataField = dataRow.get(columns.get(groups.get(i)).getFieldName());
sSummaryTitle[i] = dataField == null ? "" : dataField.toString();
}
}
int nLevelChanges = 0;
for (int i=0;i<nLevels;i++) {
if (dataRow.get( columns.get(groups.get(i)).getGroupFieldName() ) == null) {
if (i>0) {
bStartGroup[i-1] = true;
}
nLevelChanges++;
}
}
int nTotalLevel = nLevels - nLevelChanges;
if (nLevelChanges == nLevels) { // grand total row
cellClass = "grandtotal";
addCell(out, "Grand Total:", null, cellClass, nLevelChanges);
} else if (nLevelChanges > 0) { // other total row
boolean isOneLiner = (nNormalRowCount == 1);
nNormalRowCount = 0;
if (isOneLiner) continue; // skip one-line sums
cellClass = "total"+nTotalLevel;
for (int i=0;i<nLevels-nLevelChanges-1;i++) {
addCell(out," ",null,cellClass, 1);
}
addCell(out, sSummaryTitle[nLevels-nLevelChanges-1] + " total:", null, cellClass, nLevelChanges+1);
} else { // normal row
for (int i=0;i<nLevels;i++) {
if (bStartGroup[i]) {
RollupTableColumnTag column = columns.get(groups.get(i));
Object cellData = dataRow.get(column.getFieldName());
String displayVal = cellData != null ? cellData.toString() : "[n/a]";
if (column.getTooltipLink() != null && !column.getTooltipLink().isEmpty() && cellData != null) {
String tooltip = column.getTooltipLink();
int dataid = Integer.parseInt(dataRow.get(column.getGroupFieldName()).toString());
displayVal = "<div ajaxtooltip=\"" + tooltip + "\" ajaxtooltipid=\"" + dataid + "\">" + displayVal + "</div>";
}
addCell(out, displayVal, column.getAlign(), null, 1);
} else {
addCell(out," ", null, null, 1);
}
}
for (int i=0;i<nLevels-1;i++) {
bStartGroup[i] = false;
}
nNormalRowCount++;
}
// other columns
for (RollupTableColumnTag column : columns) {
if (!column.isGroup()) {
Object content = dataRow.get(column.getFieldName());
String displayVal = "";
if (content != null) {
if (column.getFormat() != null) {
float val = Float.parseFloat(content.toString());
displayVal = column.getFormatter().format(val);
} else {
displayVal = content.toString();
}
}
addCell(out,displayVal,column.getAlign(),cellClass,1);
}
}
out.append("</TR>");
// empty row for better readability
if (groups.size() > 2 && nLevelChanges == groups.size() - 1) {
out.append("<TR><TD colspan=\"" + columns.size() + "\"> </TD>");
}
writer.println(out);
}
writer.println("</TBODY>");
writer.println("</TABLE>");
} catch (IOException e) {
e.printStackTrace();
}
return EVAL_PAGE;
}
public void addCell(StringBuffer out, String content, String align, String cssClass, int colSpan) {
out.append("<TD");
if (align != null) {
out.append(" align=\"" + align + "\"");
}
if (cssClass != null) {
out.append(" class=\"" + cssClass + "\"");
}
if (colSpan > 1) {
out.append(" colspan=\"" + colSpan + "\"");
}
out.append(">");
out.append(content);
out.append("</TD>");
}
public void addColumnDefinition(RollupTableColumnTag cd) {
columns.add(cd);
if (cd.isGroup()) groups.add(columns.size()-1);
}
}