All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.seanox.pdf.Generator Maven / Gradle / Ivy

There is a newer version: 4.4.0
Show newest version
/**
 * LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt, im
 * Folgenden Seanox Software Solutions oder kurz Seanox genannt.
 * Diese Software unterliegt der Version 2 der Apache License.
 *
 * PDF Service
 * Copyright (C) 2020 Seanox Software Solutions
 *  
 * 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 com.seanox.pdf;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Generator, generates data by filling placeholders (tags) in a template/model.
 * A value list with keys is passed to the template. If the keys correspond to
 * the placeholders (upper/lower case is ignored), the placeholders are replaced
 * by the values.
*
* The generator worked at byte level.
* Values are therefore expected to be primär as byte arrays. All other * data types are converted using {@code String.valueOf(value).getBytes()}.
*
* Placeholders can also be used as segments.
* Segments are partial structures that can be nested up to a depth of 65535 * levels. These substructures can be used and filled globally or by segment * name dedicted/partially.
* The placeholders of segments remain after filling and can be reused * iteratively.
* The data types {@link Collection} and {@link Map} are expected as values for * segments. A {@link Map} then contains the values for the placeholders within * the segment. A {@link Collection} causes to an iteration over a set of * {@link Map} and is comparable to the iterative call of the method * {@link #set(String, Map)}.
* Both {@link Map} and {@link Collection} create deep, complex, possibly * repetitive and recursive structures. * *

Description of the syntax

* The syntax of the placeholders ignores upper and lower case and is limited to * the following characters: * {@code a-z A-Z 0-9 _-} * *

Structure and description of the placeholders

* * * * * * * * * * * * * *
* {@code #[value]} * * Inserts the value for <value> and removes the placeholder. *
* {@code #[scope[[...]]]} * * Defines a segment/scope. The nesting and use of further segments is * possible. Since the placeholders for inserting segments are preserved, * they can be used to build lists. *
* {@code #[0x0A]}
* {@code #[0x4578616D706C6521]} *
* Escaping from one or more characters. The conversion is done with * {@link #extract(String, Map)}, {@link #extract(String)} or * {@link #extract()} at the end of the generation. *
* *

Functionality

* The model (byte array) is parsed initially. * All placeholders are checked for syntactic correctness. * If necessary, invalid placeholders are removed. In addition, the scopes with * the segments (partial templates) are determined and replaced by a simple * placeholder. After parsing, a final model with optimized placeholders and * extracted segments is created, which cannot be changed at runtime.
*
* For the use of the model different possibilities are then available.
*
* With {@link #set(Map)} the placeholders in the model are replaced with the * values passed over. Placeholders for which no values exist are retained. * Placeholders that represent a segment/scope are also replaced if a * corresponding key exists in the values. For segments/scopes, the placeholder * is retained for reuse and directly follows the inserted value.
*
* With {@link #set(String, Map)} only the specified scope is filled. For this, * a copy of the segment (sub-template) is created and filled with the values * passed, all placeholders are removed and the content is inserted as a value * before the placeholder. Thus, this segment/scope placeholder is also * preserved for reuse.
*
* The methods {@link #extract(String)} and {@link #extract(String, Map)} use * exclusive segments (subtemplates), which are partially filled and prepared. * Both methods produce final results that correspond to the call of * {@link #set(Map)} in combination with {@link #extract()}, but focus on only * one segment.
*
* Generator 5.2 20190422
* Copyright (C) 2019 Seanox Software Solutions
* Alle Rechte vorbehalten. * * @author Seanox Software Solutions * @version 5.2 20190422 */ class Generator { /** Segments of the template */ private HashMap scopes; /** Model, data buffer of the template */ private byte[] model; /** Constructor, create an empty generator. */ private Generator() { this.scopes = new HashMap(); } /** * Creates a new generator based on the transferred template. * @param model Template as bytes * @return the generator with the template passed as bytes */ static Generator parse(byte[] model) { Generator generator = new Generator(); generator.model = generator.scan(model); return generator; } /** * Determines whether a valid placeholder starts at the specified position * in a model (fragment). In this case the length of the complete * placeholder is returned. If no placeholder can be determined, the length * 0 is returned. If no further data is available in the model for analysis * (end of data is reached) a negative value is returned. * @param model Model(Fragmet) * @param cursor Position * @return the position of the next placeholder or segment, otherwise a * negative value */ private static int scan(byte[] model, int cursor) { if (model == null || cursor >= model.length) return -1; //Phase 0: Identification of a placeholder // - supported formats: #[...], #[...[[...]]] // - characteristic are the first two characters // - all placeholders begin with #[... if (cursor +1 >= model.length || model[cursor] != '#' || model[cursor +1] != '[') return 0; int offset = cursor; int deep = 0; int[] stack = new int[65535]; while (cursor < model.length) { //The current level is determined. int level = 0; if (deep > 0) level = stack[deep]; //Phase 1: Recognition of the start of a placeholder // - supported formats: #[...], #[...[[...]]] // - characteristic are the first two characters // - all placeholders begin with #[... //A placeholder can only begin if no stack and therefore no //placeholder exists or if a segment placeholder has been determined //before. In both cases the level is not equal to 1 and another //stack with level 1 starts. if (cursor +1 < model.length && model[cursor] == '#' && model[cursor +1] == '[' && level != 1) { stack[++deep] = 1; cursor += 2; continue; } //Phase 1A: Qualification of a segment placeholder // - active level 1 is expected // - character string [[ is found //The current stack is set to level 2. if (cursor +1 < model.length && model[cursor] == '[' && model[cursor +1] == '[' && level == 1) { stack[deep] = 2; cursor += 2; continue; } //Phase 2: Detecting the end of a detected placeholder //The level must be 1 and the character [ must be found. //Then the current stack is removed, because the search is finished //here. if (model[cursor] == ']' && level == 1) { if (--deep <= 0) break; cursor += 1; continue; } //Phase 2A: Detecting the end of a detected placeholder //The level must be 1 and the character [ must be found. //Then the current stack is removed, because the search here is //completed. if (cursor +2 < model.length && model[cursor +0] == ']' && model[cursor +1] == ']' && model[cursor +2] == ']' && level == 2) { cursor += 2; if (--deep <= 0) break; cursor += 1; continue; } cursor++; } //Case 1: The stack is not empty //Thus, a placeholder was detected which is not completed. //The scan is hungry and assumes an incomplete placeholder. //Therefore the offset from start position to the end is from the model. if (deep > 0) return model.length -offset; //Case 2: The stack is empty //The placeholder was determined completely and the offset corresponds //to the length of the complete placeholder with possibly contained //segments. return cursor -offset +1; } /** * Analyzes the model and prepares it for final processing. * All placeholders are checked for syntactic correctness. Invalid * placeholders are removed if necessary. In addition, the scopes with the * segments (partial templates) are determined and replaced by a simple * placeholder. After parsing, a final model with optimized placeholders and * extracted segments is created, which cannot be changed at runtime. * @param model Model * @return the final prepared model */ private byte[] scan(byte[] model) { if (model == null) return new byte[0]; int cursor = 0; while (true) { int offset = Generator.scan(model, cursor++); if (offset < 0) break; if (offset == 0) continue; cursor--; byte[] patch = new byte[0]; String fetch = new String(model, cursor, offset); if (fetch.matches("^(?si)#\\[[a-z]([\\w\\-]*\\w)*\\[\\[.*\\]\\]\\]$")) { //scope is determined from: #[scope[[segment]] String scope = fetch.substring(2); scope = scope.substring(0, scope.indexOf('[')); scope = scope.toLowerCase(); //segment is extracted from the model byte[] cache = new byte[offset -scope.length() -7]; System.arraycopy(model, cursor +scope.length() +4, cache, 0, cache.length); //scope is registered with the segment if scope does not exist if (!this.scopes.containsKey(scope)) this.scopes.put(scope, this.scan(cache)); //as new placeholder only the scope is used patch = ("#[").concat(scope).concat("]").getBytes(); } else if (fetch.matches("^(?i)#\\[[a-z]([\\w-]*\\w)*\\]$")) { patch = fetch.toLowerCase().getBytes(); } else if (fetch.matches("^(?i)#\\[0x([0-9a-f]{2})+\\]$")) { cursor += fetch.length() +1; continue; } //model is rebuilt with the patch byte[] cache = new byte[model.length -offset +patch.length]; System.arraycopy(model, 0, cache, 0, cursor); System.arraycopy(patch, 0, cache, cursor, patch.length); System.arraycopy(model, cursor +offset, cache, cursor +patch.length, model.length -cursor -offset); model = cache; cursor += patch.length; } return model; } /** * Fills the current model with the transferred values. * Optionally, the filling can be limited to one segment by specifying a * scope and/or {@code clean} can be used to specify whether the return * value should be finalized and all outstanding placeholders removed or * resolved. * @param scope Scope or segment * @param values Values * @param clean {@code true} for final cleanup * @return the filled model (fragment) */ private byte[] assemble(String scope, Map values, boolean clean) { Iterator iterator; String label; String fetch; byte[] cache; byte[] model; byte[] patch; if (this.model == null) return new byte[0]; //Normalization of the values (lower case + smoothing of the keys) if (values == null) values = new HashMap(); iterator = values.keySet().iterator(); values = new HashMap(values); while (iterator.hasNext()) { label = (String)iterator.next(); values.put(label.toLowerCase().trim(), values.get(label)); } //Optionally the scope is determined. if (scope != null) { scope = scope.toLowerCase().trim(); //If one is specified that does not exist, nothing is to be done. if (!this.scopes.containsKey(scope)) return this.model; //Scopes are prepared independently and later processed like a //simple but exclusive placeholder. patch = this.extract(scope, values); values.clear(); values.put(scope, patch); } int cursor = 0; while (true) { int offset = Generator.scan(this.model, cursor++); if (offset < 0) break; if (offset == 0) continue; cursor--; patch = new byte[0]; fetch = new String(this.model, cursor, offset); if (fetch.matches("^(?i)#\\[[a-z]([\\w-]*\\w)*\\]$")) { fetch = fetch.substring(2, fetch.length() -1); //the placeholders of not transmitted keys are ignored, with //'clean' the placeholders are deleted if (!values.containsKey(fetch) && !clean) { cursor += fetch.length() +3 +1; continue; } //patch is determined by the key Object object = values.get(fetch); //If the key is a segment and the value is a map with values, //the segment is filled recursively. To protect against infinite //recursions, the current scope is removed from the value list. // e.g. #[A[[#[B[[#[A[[...]]...]]...]] if (this.scopes.containsKey(fetch) && object instanceof Map) { patch = this.extract(fetch, (Map)object); } else if (this.scopes.containsKey(fetch) && object instanceof Collection) { //Collections generates complex structures/tables through //deep, repetitive recursive generation. iterator = ((Collection)object).iterator(); while (iterator.hasNext()) { object = iterator.next(); if (object instanceof Map) { model = this.extract(fetch, (Map)object); } else if (object instanceof byte[]) { model = (byte[])object; } else if (object != null) { model = String.valueOf(object).getBytes(); } else continue; cache = new byte[patch.length +model.length]; System.arraycopy(patch, 0, cache, 0, patch.length); System.arraycopy(model, 0, cache, patch.length, model.length); patch = cache; } } else if (object instanceof byte[]) { patch = (byte[])object; } else if (object != null) { patch = String.valueOf(object).getBytes(); } if (!clean) { //if necessary the # characters are encoded to protect the //placeholders and structure in the model int index = 0; while (index < patch.length) { if (patch[index++] != '#') continue; cache = new byte[patch.length +6]; System.arraycopy(patch, 0, cache, 0, index); System.arraycopy(("[0x23]").getBytes(), 0, cache, index, 6); System.arraycopy(patch, index, cache, index +6, patch.length -index); patch = cache; } if (this.scopes.containsKey(fetch)) { fetch = ("#[").concat(fetch).concat("]"); cache = new byte[patch.length +fetch.length()]; System.arraycopy(patch, 0, cache, 0, patch.length); System.arraycopy(fetch.getBytes(), 0, cache, patch.length, fetch.length()); patch = cache; } } } else if (fetch.matches("^(?i)#\\[0x([0-9a-f]{2})+\\]$")) { //Hexadecimal placeholders are only resolved with clean, because //they can contain unwanted (control) characters, which hinders //rendering. if (!clean) { cursor += fetch.length() +1; continue; } //hexadecimal code is converted into bytes fetch = fetch.substring(4, fetch.length() -1); fetch = ("ff").concat(fetch); patch = new BigInteger(fetch, 16).toByteArray(); patch = Arrays.copyOfRange(patch, 2, patch.length); } //model is rebuilt with the patch cache = new byte[this.model.length -offset +patch.length]; System.arraycopy(this.model, 0, cache, 0, cursor); System.arraycopy(patch, 0, cache, cursor, patch.length); System.arraycopy(this.model, cursor +offset, cache, cursor +patch.length, this.model.length -cursor -offset); this.model = cache; cursor += patch.length; } return this.model; } /** * Return all scopes of the segments as enumeration. * Free scopes (without segment) are not included. * @return all scopes of the segments as enumeration */ Enumeration scopes() { return Collections.enumeration(this.scopes.keySet()); } /** * Returns the currently filled template. * @return the currently filled template */ byte[] extract() { return this.assemble(null, null, true).clone(); } /** * Extracts a specified segment and sets the data there. * The data of the template are not affected by this. * @param scope Segment * @return the filled segment, if this cannot be determined, an empty byte * array is returned */ byte[] extract(String scope) { return this.extract(scope, null); } /** * Extracts a specified segment and sets the data there. * The data of the template are not affected by this. * @param scope Segment * @param values List of values * @return the filled segment, if this cannot be determined, an empty byte * array is returned */ byte[] extract(String scope, Map values) { if (scope != null) scope = scope.toLowerCase().trim(); if (scope == null || !scope.matches("^[a-z]([\\w-]*\\w)*$")) return new byte[0]; //Internally, a copy of the generator is created for the segment //(partial model) and thus partially filled. Generator generator = new Generator(); generator.scopes = (HashMap)this.scopes.clone(); generator.scopes.remove(scope); generator.model = (byte[])this.scopes.get(scope); if (generator.model == null) generator.model = new byte[0]; return generator.assemble(null, values, true); } /** * Sets the data for a scope or a segment. * @param values Values */ void set(Map values) { this.set(null, values); } /** * Sets the data for a scope or a segment. * @param scope Scope or segment * @param values Values */ void set(String scope, Map values) { if (scope != null) scope = scope.toLowerCase().trim(); if (scope != null && !scope.matches("^[a-z]([\\w-]*\\w)*$")) return; this.model = this.assemble(scope, values, false); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy