net.sf.jasperreports.engine.export.JsonMetadataExporter.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jasperreports.engine.export.JsonMetadataExporter.java

Source

/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.jasperreports.engine.export;

import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import net.sf.jasperreports.annotations.properties.Property;
import net.sf.jasperreports.annotations.properties.PropertyScope;
import net.sf.jasperreports.engine.DefaultJasperReportsContext;
import net.sf.jasperreports.engine.JRAbstractExporter;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRGenericPrintElement;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRPrintHyperlink;
import net.sf.jasperreports.engine.JRPrintPage;
import net.sf.jasperreports.engine.JRPrintText;
import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JRPropertiesUtil.PropertySuffix;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.export.data.BooleanTextValue;
import net.sf.jasperreports.engine.export.data.DateTextValue;
import net.sf.jasperreports.engine.export.data.NumberTextValue;
import net.sf.jasperreports.engine.export.data.StringTextValue;
import net.sf.jasperreports.engine.export.data.TextValue;
import net.sf.jasperreports.engine.export.data.TextValueHandler;
import net.sf.jasperreports.engine.type.EnumUtil;
import net.sf.jasperreports.engine.type.NamedEnum;
import net.sf.jasperreports.engine.util.JRDataUtils;
import net.sf.jasperreports.engine.util.JRStyledText;
import net.sf.jasperreports.export.ExportInterruptedException;
import net.sf.jasperreports.export.ExporterInputItem;
import net.sf.jasperreports.export.JsonExporterConfiguration;
import net.sf.jasperreports.export.JsonMetadataReportConfiguration;
import net.sf.jasperreports.export.WriterExporterOutput;
import net.sf.jasperreports.properties.PropertyConstants;

/**
 * @author Narcis Marcu (narcism@users.sourceforge.net)
 */
public class JsonMetadataExporter extends
        JRAbstractExporter<JsonMetadataReportConfiguration, JsonExporterConfiguration, WriterExporterOutput, JsonExporterContext> {

    private static final Log log = LogFactory.getLog(JsonMetadataExporter.class);

    public static final String JSON_EXPORTER_KEY = JRPropertiesUtil.PROPERTY_PREFIX + "json";

    protected static final String JSON_EXPORTER_PROPERTIES_PREFIX = JRPropertiesUtil.PROPERTY_PREFIX
            + "export.json.";

    protected static final String EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT = "export.json.invalid.json.object";
    protected static final String EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT_SEMANTIC = EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT
            + ".semantic";
    protected static final String EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT_ARRAY_FOUND = EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT
            + ".array.found";

    @Property(category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_0_0)
    public static final String JSON_EXPORTER_PATH_PROPERTY = JSON_EXPORTER_PROPERTIES_PREFIX + "path";
    @Property(category = PropertyConstants.CATEGORY_EXPORT, defaultValue = PropertyConstants.BOOLEAN_FALSE, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_0_0, valueType = Boolean.class)
    public static final String JSON_EXPORTER_REPEAT_VALUE_PROPERTY = JSON_EXPORTER_PROPERTIES_PREFIX
            + "repeat.value";
    @Property(category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_0_0)
    public static final String JSON_EXPORTER_DATA_PROPERTY = JSON_EXPORTER_PROPERTIES_PREFIX + "data";

    @Property(name = "net.sf.jasperreports.export.json.repeat.{path}", category = PropertyConstants.CATEGORY_EXPORT, defaultValue = PropertyConstants.BOOLEAN_FALSE, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_1_0, valueType = Boolean.class)
    public static final String JSON_EXPORTER_REPEAT_PROPERTIES_PREFIX = JSON_EXPORTER_PROPERTIES_PREFIX + "repeat.";
    @Property(name = "net.sf.jasperreports.export.json.number.{path}", category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_1_0)
    public static final String JSON_EXPORTER_NUMBER_PROPERTIES_PREFIX = JSON_EXPORTER_PROPERTIES_PREFIX + "number.";
    @Property(name = "net.sf.jasperreports.export.json.date.{path}", category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_1_0)
    public static final String JSON_EXPORTER_DATE_PROPERTIES_PREFIX = JSON_EXPORTER_PROPERTIES_PREFIX + "date.";
    @Property(name = "net.sf.jasperreports.export.json.boolean.{path}", category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_1_0)
    public static final String JSON_EXPORTER_BOOLEAN_PROPERTIES_PREFIX = JSON_EXPORTER_PROPERTIES_PREFIX
            + "boolean.";
    @Property(name = "net.sf.jasperreports.export.json.string.{path}", category = PropertyConstants.CATEGORY_EXPORT, scopes = {
            PropertyScope.ELEMENT }, sinceVersion = PropertyConstants.VERSION_6_1_0)
    public static final String JSON_EXPORTER_STRING_PROPERTIES_PREFIX = JSON_EXPORTER_PROPERTIES_PREFIX + "string.";

    private static final String JSON_SCHEMA_ROOT_NAME = "___root";

    protected final DateFormat isoDateFormat = JRDataUtils.getIsoDateFormat();

    protected Writer writer;
    protected int reportIndex;
    protected int pageIndex;

    private Map<String, SchemaNode> pathToValueNode = new HashMap<String, SchemaNode>();
    private Map<String, SchemaNode> pathToObjectNode = new HashMap<String, SchemaNode>();
    private Map<SchemaNode, ArrayList<String>> visitedMembers = new HashMap<SchemaNode, ArrayList<String>>();

    private ArrayList<SchemaNode> openedSchemaNodes = new ArrayList<SchemaNode>();

    private String jsonSchema;
    private String previousPath;
    private boolean escapeMembers;
    private boolean gotSchema;

    public void validateSchema(String jsonSchema) throws JRException {
        ObjectMapper mapper = new ObjectMapper();

        // relax the JSON rules
        mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);

        try {
            JsonNode root = mapper.readTree(jsonSchema);
            if (root.isObject()) {
                pathToValueNode = new HashMap<String, SchemaNode>();
                pathToObjectNode = new HashMap<String, SchemaNode>();

                previousPath = null;

                if (!isValid((ObjectNode) root, JSON_SCHEMA_ROOT_NAME, "", null)) {
                    throw new JRException(EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT_SEMANTIC, (Object[]) null);
                }
            } else {
                throw new JRException(EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT_ARRAY_FOUND, (Object[]) null);
            }

        } catch (IOException e) {
            throw new JRException(EXCEPTION_MESSAGE_KEY_INVALID_JSON_OBJECT, (Object[]) null);
        }
    }

    private boolean isValid(ObjectNode objectNode, String objectName, String currentPath, SchemaNode parent) {
        String nodeTypeValue = null;
        JsonNode typeNode = objectNode.path("_type");

        if (typeNode.isMissingNode()) {
            nodeTypeValue = "object";
        } else if (!typeNode.isTextual()) {
            return false;
        }

        if (nodeTypeValue == null) {
            nodeTypeValue = typeNode.asText();
        }

        NodeTypeEnum nodeType = NodeTypeEnum.getByName(nodeTypeValue);

        // enforce type "object" or "array" with "_children"
        if (!(NodeTypeEnum.OBJECT.equals(nodeType)
                || (NodeTypeEnum.ARRAY.equals(nodeType) && objectNode.has("_children")))) {
            return false;
        }

        // enforce "_children" of type Object when typeNode is "array"
        if (NodeTypeEnum.ARRAY.equals(nodeType) && !objectNode.path("_children").isObject()) {
            return false;
        }

        boolean result = true;
        String availablePath = currentPath;
        SchemaNode schemaNode;

        availablePath = availablePath.length() > 0
                ? (availablePath.endsWith(".") ? availablePath : availablePath + ".") + objectName
                : objectName;

        // _children properties are passed to the parent array
        if (parent != null) {
            schemaNode = parent;
        } else {

            int level;

            if (JSON_SCHEMA_ROOT_NAME.equals(objectName)) {
                level = 0;
            } else if (availablePath.length() > 0 && availablePath.indexOf(".") > 0) {
                level = availablePath.split("\\.").length - 1;
            } else {
                level = 1;
            }

            schemaNode = new SchemaNode(level, objectName, nodeType,
                    currentPath.endsWith(".") ? currentPath.substring(0, currentPath.length() - 2) : currentPath);
            pathToObjectNode.put(availablePath, schemaNode);
        }

        Iterator<String> it = objectNode.fieldNames();
        while (it.hasNext()) {
            String field = it.next();
            JsonNode node = objectNode.path(field);
            String localPath = availablePath;

            if (!field.startsWith("_")) {
                schemaNode.addMember(field);
                if (node.isTextual() && node.asText().equals("value")) {
                    localPath = localPath.length() > 0
                            ? (localPath.endsWith(".") ? localPath : localPath + ".") + field
                            : field;
                    pathToValueNode.put(localPath, schemaNode);
                } else if ((node.isObject() && !isValid((ObjectNode) node, field, availablePath, null))
                        || !node.isObject()) {
                    result = false;
                    break;
                }
            } else if (field.equals("_children") && !isValid((ObjectNode) node, "", availablePath, schemaNode)) {
                result = false;
                break;
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("object is valid: " + objectNode);
            log.debug("objectName: " + objectName);
            log.debug("currentPath: " + currentPath);
        }

        return result;
    }

    public JsonMetadataExporter() {
        this(DefaultJasperReportsContext.getInstance());
    }

    public JsonMetadataExporter(JasperReportsContext jasperReportsContext) {
        super(jasperReportsContext);

        exporterContext = new ExporterContext();
    }

    @Override
    protected Class<JsonExporterConfiguration> getConfigurationInterface() {
        return JsonExporterConfiguration.class;
    }

    @Override
    protected Class<JsonMetadataReportConfiguration> getItemConfigurationInterface() {
        return JsonMetadataReportConfiguration.class;
    }

    @Override
    @SuppressWarnings("deprecation")
    protected void ensureOutput() {
        if (exporterOutput == null) {
            exporterOutput = new net.sf.jasperreports.export.parameters.ParametersWriterExporterOutput(
                    getJasperReportsContext(), getParameters(), getCurrentJasperPrint());
        }
    }

    @Override
    public String getExporterKey() {
        return JSON_EXPORTER_KEY;
    }

    @Override
    public String getExporterPropertiesPrefix() {
        return JSON_EXPORTER_PROPERTIES_PREFIX;
    }

    @Override
    public void exportReport() throws JRException {
        /*   */
        ensureJasperReportsContext();
        ensureInput();

        //FIXMENOW check all exporter properties that are supposed to work at report level

        initExport();

        ensureOutput();

        writer = getExporterOutput().getWriter();

        try {
            exportReportToWriter();
        } catch (IOException e) {
            throw new JRException(EXCEPTION_MESSAGE_KEY_OUTPUT_WRITER_ERROR, new Object[] { jasperPrint.getName() },
                    e);
        } finally {
            getExporterOutput().close();
            resetExportContext();//FIXMEEXPORT check if using same finally is correct; everywhere
        }
    }

    @Override
    protected void initExport() {
        super.initExport();
    }

    @Override
    protected void initReport() {
        super.initReport();
    }

    protected void exportReportToWriter() throws JRException, IOException {
        List<ExporterInputItem> items = exporterInput.getItems();

        for (reportIndex = 0; reportIndex < items.size(); reportIndex++)//FIXMEJSONMETA deal with batch export
        {
            ExporterInputItem item = items.get(reportIndex);

            setCurrentExporterInputItem(item);

            JsonMetadataReportConfiguration currentItemConfiguration = getCurrentItemConfiguration();

            escapeMembers = currentItemConfiguration.isEscapeMembers();
            String jsonSchemaResource = currentItemConfiguration.getJsonSchemaResource();

            if (jsonSchemaResource != null) {
                InputStream is = null;
                try {
                    is = getRepository().getInputStreamFromLocation(jsonSchemaResource);
                    jsonSchema = new Scanner(is, "UTF-8").useDelimiter("\\A").next();
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                        }
                    }
                }

                validateSchema(jsonSchema);
                gotSchema = true;
            } else {
                if (log.isWarnEnabled()) {
                    log.warn("No JSON Schema provided!");
                }
            }

            List<JRPrintPage> pages = jasperPrint.getPages();
            if (pages != null && pages.size() > 0) {
                PageRange pageRange = getPageRange();
                int startPageIndex = (pageRange == null || pageRange.getStartPageIndex() == null) ? 0
                        : pageRange.getStartPageIndex();
                int endPageIndex = (pageRange == null || pageRange.getEndPageIndex() == null) ? (pages.size() - 1)
                        : pageRange.getEndPageIndex();

                JRPrintPage page = null;
                for (pageIndex = startPageIndex; pageIndex <= endPageIndex; pageIndex++) {
                    if (Thread.interrupted()) {
                        throw new ExportInterruptedException();
                    }

                    page = pages.get(pageIndex);

                    exportPage(page);
                }

                closeOpenNodes();
            }

            if (log.isDebugEnabled()) {
                for (Map.Entry<String, SchemaNode> entry : pathToValueNode.entrySet()) {
                    log.debug("pathToValueNode: path: " + entry.getKey() + "; node: " + entry.getValue());
                }

                for (Map.Entry<String, SchemaNode> entry : pathToObjectNode.entrySet()) {
                    log.debug("pathToObjectNode: path: " + entry.getKey() + "; node: " + entry.getValue());
                }
            }
        }

        boolean flushOutput = getCurrentConfiguration().isFlushOutput();
        if (flushOutput) {
            writer.flush();
        }
    }

    protected void exportPage(JRPrintPage page) throws IOException {
        Collection<JRPrintElement> elements = page.getElements();

        exportElements(elements);

        JRExportProgressMonitor progressMonitor = getCurrentItemConfiguration().getProgressMonitor();
        if (progressMonitor != null) {
            progressMonitor.afterPageExport();
        }
    }

    protected void exportElements(Collection<JRPrintElement> elements) throws IOException {
        if (elements != null && elements.size() > 0) {
            for (Iterator<JRPrintElement> it = elements.iterator(); it.hasNext();) {
                JRPrintElement element = it.next();

                if (filter == null || filter.isToExport(element)) {
                    exportElement(element);

                    if (element instanceof JRGenericPrintElement) {
                        //exportElement(element);
                    } else if (element instanceof JRPrintFrame) {
                        exportElements(((JRPrintFrame) element).getElements());
                    }
                }
            }
        }
    }

    protected void exportElement(JRPrintElement element) throws IOException {
        JRPropertiesMap propMap = element.getPropertiesMap();

        List<PropertySuffix> properties = JRPropertiesUtil.getProperties(element, JSON_EXPORTER_PROPERTIES_PREFIX);

        for (PropertySuffix property : properties) {
            String propertyPath = null;
            boolean repeatValue = false;
            Object value = null;
            boolean legacyPathProperty = false;

            String propertyName = property.getKey();

            if (propertyName.equals(JSON_EXPORTER_PATH_PROPERTY)) {
                legacyPathProperty = true;
                propertyPath = property.getValue();
                repeatValue = getPropertiesUtil().getBooleanProperty(propMap, JSON_EXPORTER_REPEAT_VALUE_PROPERTY,
                        false);
            } else if (propertyName.startsWith(JSON_EXPORTER_STRING_PROPERTIES_PREFIX)) {
                propertyPath = propertyName.substring(JSON_EXPORTER_STRING_PROPERTIES_PREFIX.length());
                repeatValue = getPropertiesUtil().getBooleanProperty(propMap,
                        JSON_EXPORTER_REPEAT_PROPERTIES_PREFIX + propertyPath, false);
                value = property.getValue();
            } else if (propertyName.startsWith(JSON_EXPORTER_NUMBER_PROPERTIES_PREFIX)) {
                propertyPath = propertyName.substring(JSON_EXPORTER_NUMBER_PROPERTIES_PREFIX.length());
                repeatValue = getPropertiesUtil().getBooleanProperty(propMap,
                        JSON_EXPORTER_REPEAT_PROPERTIES_PREFIX + propertyPath, false);
                value = Double.parseDouble(property.getValue());
            } else if (propertyName.startsWith(JSON_EXPORTER_DATE_PROPERTIES_PREFIX)) {
                propertyPath = propertyName.substring(JSON_EXPORTER_DATE_PROPERTIES_PREFIX.length());
                repeatValue = getPropertiesUtil().getBooleanProperty(propMap,
                        JSON_EXPORTER_REPEAT_PROPERTIES_PREFIX + propertyPath, false);
                try {
                    value = isoDateFormat.parse(property.getValue());
                } catch (ParseException e) {
                    throw new JRRuntimeException(e);
                }
            } else if (propertyName.startsWith(JSON_EXPORTER_BOOLEAN_PROPERTIES_PREFIX)) {
                propertyPath = propertyName.substring(JSON_EXPORTER_BOOLEAN_PROPERTIES_PREFIX.length());
                repeatValue = getPropertiesUtil().getBooleanProperty(propMap,
                        JSON_EXPORTER_REPEAT_PROPERTIES_PREFIX + propertyPath, false);
                value = Boolean.parseBoolean(property.getValue());
            }

            if (propertyPath != null && propertyPath.length() > 0) {
                String absolutePath = JSON_SCHEMA_ROOT_NAME + "." + propertyPath;

                // we have a mapped node for this path
                if (gotSchema) {
                    if (pathToValueNode.containsKey(absolutePath)) {
                        if (log.isDebugEnabled()) {
                            log.debug("found element with path: " + propertyPath);
                        }

                        if (legacyPathProperty) {
                            value = getValue(element);
                        }

                        processElement(value, absolutePath, repeatValue);
                    }
                } else {
                    prepareSchema(absolutePath);
                    if (log.isDebugEnabled()) {
                        log.debug("found element with path: " + propertyPath);
                    }

                    if (legacyPathProperty) {
                        value = getValue(element);
                    }

                    processElement(value, absolutePath, repeatValue);
                }
            }
        }
    }

    private void prepareSchema(String absolutePath) {
        if (!pathToValueNode.containsKey(absolutePath)) {
            String valueProperty = absolutePath.substring(absolutePath.lastIndexOf(".") + 1);
            String[] objectPathSegments = absolutePath.substring(0, absolutePath.lastIndexOf(".")).split("\\.");
            SchemaNode node = null;

            for (int i = 0; i < objectPathSegments.length; i++) {
                StringBuilder objectPath = new StringBuilder(objectPathSegments[0]);
                for (int j = 1; j <= i; j++) {
                    objectPath.append(".").append(objectPathSegments[j]);
                }

                if (!pathToObjectNode.containsKey(objectPath.toString())) {
                    String schemaNodePath = "";

                    for (int k = 0; k < i; k++) {
                        schemaNodePath += schemaNodePath.length() > 0 ? "." + objectPathSegments[k]
                                : objectPathSegments[k];
                    }

                    node = new SchemaNode(i, objectPathSegments[i], NodeTypeEnum.ARRAY, schemaNodePath);

                    pathToObjectNode.put(objectPath.toString(), node);
                } else {
                    node = pathToObjectNode.get(objectPath.toString());
                }

                if (i < objectPathSegments.length - 1 && node.getMember(objectPathSegments[i + 1]) == null) {
                    node.addMember(objectPathSegments[i + 1]);
                }
            }

            node.addMember(valueProperty);
            pathToValueNode.put(absolutePath, node);
        }
    }

    private Object getValue(JRPrintElement element) throws IOException {
        Object value;
        final String textStr;
        final boolean hasDataProp;
        if (element.getPropertiesMap().containsProperty(JSON_EXPORTER_DATA_PROPERTY)) {
            hasDataProp = true;
            textStr = element.getPropertiesMap().getProperty(JSON_EXPORTER_DATA_PROPERTY);
        } else {
            hasDataProp = false;
            if (element instanceof JRPrintText) {
                JRPrintText printText = (JRPrintText) element;
                JRStyledText styledText = getStyledText(printText);

                if (styledText != null) {
                    textStr = styledText.getText();
                } else {
                    textStr = null;
                }
            } else {
                textStr = null;
            }
        }

        if (element instanceof JRPrintText) {
            JRPrintText printText = (JRPrintText) element;
            TextValue textValue = getTextValue(printText, textStr);
            LocalTextValueHandler handler = new LocalTextValueHandler(hasDataProp, textStr);
            try {
                textValue.handle(handler);
            } catch (JRException e) {
                throw new JRRuntimeException(e);
            }
            value = handler.getValue();
        } else {
            value = textStr;
        }

        return value;
    }

    private void processElement(Object value, String absolutePath, boolean repeatValue) throws IOException {
        if (openedSchemaNodes.size() == 0) {
            // initialize the json for the first time
            initJson(absolutePath, value, repeatValue);
        } else {
            String valueProperty = absolutePath.substring(absolutePath.lastIndexOf(".") + 1);

            String[] curSegments = absolutePath.substring(0, absolutePath.lastIndexOf(".")).split("\\.");
            String[] prevSegments = previousPath.substring(0, previousPath.lastIndexOf(".")).split("\\.");

            int ln = Math.min(curSegments.length, prevSegments.length);
            int lastCommonIndex = -1;

            for (int i = 0; i < ln; i++) {
                if (curSegments[i].equals(prevSegments[i])) {
                    lastCommonIndex = i;
                } else {
                    break;
                }
            }

            int commonSegmentsNo = lastCommonIndex + 1;

            // compared to previous path, we have different path with common segments
            if (commonSegmentsNo < prevSegments.length) {
                if (log.isDebugEnabled()) {
                    log.debug("\tgot different path with common segments");
                }

                // close the extra path segments of the previous path
                closeExtraPathSegments(prevSegments, lastCommonIndex);

                // open new path segments for the current path
                openPathSegments(curSegments, lastCommonIndex + 1);
            }
            // we have a longer path that extends previous path
            else if (commonSegmentsNo == prevSegments.length && curSegments.length > prevSegments.length) {
                if (log.isDebugEnabled()) {
                    log.debug("\tgot longer path than previous one");
                }

                // open new paths
                openPathSegments(curSegments, lastCommonIndex + 1);
            }

            SchemaNode currentNode = pathToValueNode.get(absolutePath);

            if (log.isDebugEnabled()) {
                log.debug("\tcurrent node is: " + currentNode.getType().getName());
            }

            if (currentNode.isArray()) {
                writePathProperty(currentNode, valueProperty, value, repeatValue);
            }
            // just write the value for property, no repeat
            else {
                writePathProperty(currentNode, valueProperty, value, false);
            }
        }

        previousPath = absolutePath;
    }

    private void writePathProperty(SchemaNode node, String valueProperty, Object value, boolean repeatValue)
            throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("\twriting property: " + valueProperty);
        }
        ArrayList<String> vizMembers = visitedMembers.get(node);
        String lastProp = null;
        int lastPropIdx = -1;
        int valPropIdx = node.indexOfMember(valueProperty);

        if (vizMembers != null && vizMembers.size() > 0) {
            lastProp = vizMembers.get(vizMembers.size() - 1);
            lastPropIdx = node.indexOfMember(lastProp);
        } else {
            vizMembers = new ArrayList<String>();
            visitedMembers.put(node, vizMembers);
        }

        boolean foundPreviousRepeated = false;

        // if property of the same object
        if (lastProp == null || valPropIdx > lastPropIdx) {
            if (log.isDebugEnabled()) {
                log.debug("\tgot property of the same object");
            }

            // check for repeated values, if any, before writing current
            if (lastProp != null) {
                foundPreviousRepeated = writeReapeatedValues(node, lastPropIdx + 1, valPropIdx);
            } else {
                foundPreviousRepeated = writeReapeatedValues(node, 0, valPropIdx);
            }

            if (foundPreviousRepeated || vizMembers.size() > 0) {
                writer.write(",\n");
            }

            writeEscaped(node, valueProperty, value, repeatValue);

            // mark visited property for current node
            visitedMembers.get(node).add(valueProperty);
        }
        // create new object
        else {
            if (log.isDebugEnabled()) {
                log.debug("\tgot property of a new object");
            }
            // before closing current object, write the repeated values, if any, from last accessed property until the end is reached
            writeReapeatedValues(node, lastPropIdx + 1, node.getMembers().size());

            // close existing object
            writer.write("},\n{");

            // check for repeated values, if any, before writing current
            foundPreviousRepeated = writeReapeatedValues(node, 0, valPropIdx);

            if (foundPreviousRepeated) {
                writer.write(",");
            }

            writeEscaped(node, valueProperty, value, repeatValue);

            // mark visited property for current node
            visitedMembers.get(node).clear();
            visitedMembers.get(node).add(valueProperty);
        }
    }

    private boolean writeReapeatedValues(SchemaNode node, int from, int to) throws IOException {
        return writeReapeatedValues(node, from, to, true);
    }

    private boolean writeReapeatedValues(SchemaNode node, int from, int to, boolean startWithComma)
            throws IOException {
        boolean found = false;
        SchemaNodeMember member;

        for (int i = from; i < to; i++) {
            member = node.getMember(i);
            if (member.isRepeatValue() && member.getPreviousValue() != null) {
                found = true;
                if (i != 0 && startWithComma) {
                    writer.write(",");
                }
                if (escapeMembers) {
                    writer.write("\"" + member.getName() + "\":");
                } else {
                    writer.write(member.getName() + ":");
                }

                writeValue(member.getPreviousValue());

                if (log.isDebugEnabled()) {
                    log.debug("\t\twriting repeated value for member: " + member.getName());
                }
            }
        }

        return found;
    }

    private void writeEscaped(SchemaNode node, String valueProperty, Object value, boolean repeatValue)
            throws IOException {
        // write current value
        if (escapeMembers) {
            writer.write("\"" + valueProperty + "\":");
        } else {
            writer.write(valueProperty + ":");
        }

        writeValue(value);

        // mark repeated value
        if (repeatValue) {
            SchemaNodeMember nodeMember = node.getMember(valueProperty);
            nodeMember.setRepeatValue(true);
            nodeMember.setPreviousValue(value);
        }
    }

    private void closeExtraPathSegments(String[] prevSegments, int lastCommonIndex) throws IOException {
        for (int i = prevSegments.length - 1; i > lastCommonIndex; i--) {
            StringBuilder sb = new StringBuilder(prevSegments[0]);
            for (int j = 1; j <= i; j++) {
                sb.append(".").append(prevSegments[j]);
            }

            SchemaNode toClose = pathToObjectNode.get(sb.toString());

            if (openedSchemaNodes.get(openedSchemaNodes.size() - 1).equals(toClose)) {
                openedSchemaNodes.remove(openedSchemaNodes.size() - 1);
            } else if (log.isWarnEnabled()) {
                log.warn("unexpected");
            }

            // write previous repeated before closing
            if (toClose.isArray()) {
                List<String> vizMembers = visitedMembers.get(toClose);
                String lastProp = vizMembers.get(vizMembers.size() - 1);
                int lastPropIdx = toClose.indexOfMember(lastProp);
                writeReapeatedValues(toClose, lastPropIdx + 1, toClose.getMembers().size());

                // clear visited member cache for closed node
                vizMembers.clear();
            }

            if (toClose.isObject()) {
                writer.write("}\n");
            } else {
                writer.write("}]\n");
            }

            if (log.isDebugEnabled()) {
                log.debug("\t\tclosing " + toClose.getType().getName() + " path: " + sb.toString());
            }
        }
    }

    private void openPathSegments(String[] pathSegments, int from) throws IOException {
        for (int i = from; i < pathSegments.length; i++) {
            StringBuilder sb = new StringBuilder(pathSegments[0]);
            StringBuilder parentPath = new StringBuilder(pathSegments[0]);
            for (int j = 1; j <= i; j++) {
                sb.append(".").append(pathSegments[j]);
                if (j < i) {
                    parentPath.append(".").append(pathSegments[j]);
                }
            }

            SchemaNode parent = pathToObjectNode.get(parentPath.toString());
            String currentProperty = pathSegments[i];
            boolean foundPreviousRepeated = false;

            ArrayList<String> vizMembers = visitedMembers.get(parent);
            String lastVisitedProp = null;
            int lastVisitedPropIdx = -1;
            int currentPropIdx = parent.indexOfMember(currentProperty);

            if (vizMembers != null && vizMembers.size() > 0) {
                lastVisitedProp = vizMembers.get(vizMembers.size() - 1);
                lastVisitedPropIdx = parent.indexOfMember(lastVisitedProp);
            }

            // before opening new path, check if previous has repeated values to be written
            if (parent.isArray()) {
                if (lastVisitedProp != null) {
                    foundPreviousRepeated = writeReapeatedValues(parent, lastVisitedPropIdx + 1, currentPropIdx,
                            false);
                } else {
                    vizMembers = new ArrayList<String>();
                    visitedMembers.put(parent, vizMembers);
                }

                vizMembers.add(currentProperty);
            }

            if (foundPreviousRepeated ||
            // got another property of the same object
                    (lastVisitedPropIdx != -1 && currentPropIdx > lastVisitedPropIdx)) {
                writer.write(",");
            }

            if (escapeMembers) {
                writer.write("\"" + currentProperty + "\":");
            } else {
                writer.write(currentProperty + ":");
            }

            SchemaNode toOpen = pathToObjectNode.get(sb.toString());

            openedSchemaNodes.add(toOpen);

            if (toOpen.isObject()) {
                writer.write("{");
            } else {
                writer.write("[{");
            }

            if (log.isDebugEnabled()) {
                log.debug("\t\topening " + toOpen.getType().getName() + " path: " + sb.toString());
            }
        }
    }

    private void closeOpenNodes() throws IOException {
        if (openedSchemaNodes.size() == 0) {
            return;
        }

        SchemaNode toClose;
        for (int i = openedSchemaNodes.size() - 1; i >= 0; i--) {
            toClose = openedSchemaNodes.get(i);
            if (toClose.isArray()) {
                // write previous repeated before closing
                List<String> vizMembers = visitedMembers.get(toClose);

                String lastProp = vizMembers.get(vizMembers.size() - 1);
                int lastPropIdx = toClose.indexOfMember(lastProp);
                writeReapeatedValues(toClose, lastPropIdx + 1, toClose.getMembers().size());

                // clear visited member cache for closed node
                vizMembers.clear();

                writer.write("}]");
            } else {
                writer.write("}");
            }

            if (log.isDebugEnabled()) {
                log.debug("closing " + toClose.getType().getName() + " path: "
                        + (toClose.path.length() > 0 ? toClose.path + "." : "") + toClose.name);
            }
        }
    }

    private void initJson(String firstPath, Object firstValue, boolean repeatValue) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("Initializing JSON with first absolute path: " + firstPath);
        }
        String[] segments = firstPath.split("\\.");

        String currentPath = "";
        SchemaNode schemaNode = null;
        int i;

        for (i = 0; i < segments.length - 1; i++) {
            currentPath = currentPath.length() > 0 ? currentPath + "." + segments[i] : segments[i];
            schemaNode = pathToObjectNode.get(currentPath);

            openedSchemaNodes.add(schemaNode);

            if (i == 0) { // got root node
                if (schemaNode.isObject()) {
                    writer.write("{");
                } else {
                    writer.write("[{");
                }
            } else {
                String parentPath = currentPath.substring(0, currentPath.lastIndexOf("."));
                SchemaNode parent = pathToObjectNode.get(parentPath);
                String currentProperty = segments[i];

                ArrayList<String> vizMembers = new ArrayList<String>();
                vizMembers.add(currentProperty);
                visitedMembers.put(parent, vizMembers);

                if (schemaNode.isObject()) {
                    if (escapeMembers) {
                        writer.write("\"" + currentProperty + "\": {");
                    } else {
                        writer.write(currentProperty + ": {");
                    }
                } else {
                    if (escapeMembers) {
                        writer.write("\"" + currentProperty + "\": [{");
                    } else {
                        writer.write(currentProperty + ": [{");
                    }
                }
            }
        }

        if (escapeMembers) {
            writer.write("\"" + segments[i] + "\": ");
        } else {
            writer.write(segments[i] + ": ");
        }
        writeValue(firstValue);

        // mark repeated value
        if (schemaNode != null && repeatValue) {
            SchemaNodeMember nodeMember = schemaNode.getMember(segments[i]);
            nodeMember.setRepeatValue(true);
            nodeMember.setPreviousValue(firstValue);
        }

        // mark visited property for current node
        ArrayList<String> members = new ArrayList<String>();
        members.add(segments[i]);
        visitedMembers.put(schemaNode, members);
    }

    private void writeValue(Object value) throws IOException {
        if (value != null) {
            if (value instanceof Number || value instanceof Boolean) {
                writer.write(value.toString());
            } else if (value instanceof Date) {
                writer.write("\"");
                writer.write(isoDateFormat.format((Date) value));
                writer.write("\"");
            } else {
                writer.write("\"");
                writer.write(JsonStringEncoder.getInstance().quoteAsString(value.toString()));
                writer.write("\"");
            }
        } else {
            writer.write("null"); // FIXMEJSONMETA: how to treat null values?
        }
    }

    @Override
    protected JRStyledText getStyledText(JRPrintText textElement) {
        return textElement.getFullStyledText(noneSelector);
    }

    protected class ExporterContext extends BaseExporterContext implements JsonExporterContext {
        @Override
        public String getHyperlinkURL(JRPrintHyperlink link) {
            return ""; // FIXMEJSONMETA should we treat hyperlinks?
        }
    }

    private class SchemaNode {
        private int level;
        private String name;
        private NodeTypeEnum type;
        private String path;
        private List<SchemaNodeMember> members;
        private List<String> memberNames;

        public SchemaNode(int _level, String _name, NodeTypeEnum _type, String _path) {
            level = _level;
            name = _name;
            type = _type;
            path = _path;
            members = new ArrayList<SchemaNodeMember>();
            memberNames = new ArrayList<String>();
        }

        public NodeTypeEnum getType() {
            return type;
        }

        public void addMember(String memberName) {
            members.add(new SchemaNodeMember(memberName));
            memberNames.add(memberName);
        }

        public boolean isObject() {
            return NodeTypeEnum.OBJECT.equals(type);
        }

        public boolean isArray() {
            return NodeTypeEnum.ARRAY.equals(type);
        }

        public int indexOfMember(String memberName) {
            return memberNames.indexOf(memberName);
        }

        public SchemaNodeMember getMember(int i) {
            return members.get(i);
        }

        public SchemaNodeMember getMember(String memberName) {
            if (indexOfMember(memberName) != -1) {
                return members.get(indexOfMember(memberName));
            } else {
                return null;
            }
        }

        public List<SchemaNodeMember> getMembers() {
            return members;
        }

        @Override
        public String toString() {
            StringBuilder out = new StringBuilder("{");
            boolean isArray = NodeTypeEnum.ARRAY.equals(type);

            out.append("level: ").append(level).append(", ");
            out.append("name: \"").append(name).append("\", ");
            out.append("type: \"").append(type.getName()).append("\", ");
            out.append("path: \"").append(path).append("\", ");
            out.append("members: [");
            if (isArray) {
                out.append("{");
            }
            for (int i = 0, ln = members.size(); i < ln; i++) {
                out.append("\"").append(members.get(i).getName()).append("\"");
                if (i < ln - 1) {
                    out.append(", ");
                }
            }
            if (isArray) {
                out.append("}");
            }
            out.append("]}");
            return out.toString();
        }

        @Override
        public boolean equals(Object obj) {
            return this.level == ((SchemaNode) obj).level && this.name.equals(((SchemaNode) obj).name)
                    && this.type.equals(((SchemaNode) obj).type) && this.path.equals(((SchemaNode) obj).path);
        }

        @Override
        public int hashCode() {
            int hash = level != 0 ? level : 41;
            hash = hash * 41 + name.hashCode();
            hash = hash * 41 + type.hashCode();
            hash = hash * 41 + path.hashCode();
            return hash;
        }
    }

    private class SchemaNodeMember {

        private boolean repeatValue;
        private Object previousValue;
        private String name;

        public SchemaNodeMember(String _name) {
            name = _name;
        }

        public String getName() {
            return name;
        }

        public boolean isRepeatValue() {
            return repeatValue;
        }

        public void setRepeatValue(boolean _repeatValue) {
            repeatValue = _repeatValue;
        }

        public Object getPreviousValue() {
            return previousValue;
        }

        public void setPreviousValue(Object _previousValue) {
            previousValue = _previousValue;
        }

    }

    private enum NodeTypeEnum implements NamedEnum {
        /**
         *
         */
        OBJECT("object"),

        /**
         *
         */
        ARRAY("array");

        private final String name;

        private NodeTypeEnum(String _name) {
            name = _name;
        }

        @Override
        public String getName() {
            return name;
        }

        public static NodeTypeEnum getByName(String name) {
            return EnumUtil.getEnumByName(values(), name);
        }
    }

    private class LocalTextValueHandler implements TextValueHandler {
        Object value;
        boolean hasDataProp;
        String textStr;

        public LocalTextValueHandler(boolean hasDataProp, String textStr) {
            this.hasDataProp = hasDataProp;
            this.textStr = textStr;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public void handle(StringTextValue textValue) {
            value = textValue.getText();
        }

        @Override
        public void handle(NumberTextValue textValue) {
            if (hasDataProp) {
                if (textStr != null) {
                    try {
                        value = Double.parseDouble(textStr);
                    } catch (NumberFormatException nfe) {
                        throw new JRRuntimeException(nfe);
                    }
                }
            } else {
                value = textValue.getValue();
            }
        }

        @Override
        public void handle(DateTextValue textValue) {
            if (hasDataProp) {
                if (textStr != null) {
                    try {
                        value = new Date(Long.parseLong(textStr));
                    } catch (NumberFormatException nfe) {
                        try {
                            value = isoDateFormat.parse(textStr);
                        } catch (ParseException pe) {
                            throw new JRRuntimeException(pe);
                        }
                    }
                }
            } else {
                value = textValue.getValue();
            }
        }

        @Override
        public void handle(BooleanTextValue textValue) {
            value = hasDataProp ? Boolean.valueOf(textStr) : textValue.getValue();
        }

    }
}