/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sling.commons.json.jcr;

import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Set;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;

import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.io.JSONWriter;

/** Dumps JCR Items as JSON data. The dump methods
 *  are threadsafe.
 */
public class JsonItemWriter {

    private static DateFormat calendarFormat;

    private final Set<String> propertyNamesToIgnore;

    /** Create a JsonItemWriter
     *  @param propertyNamesToIgnore if not null, a property having a name from this
     *  set of values is ignored.
     *  TODO we should use a filtering interface to make the selection of which Nodes
     *  and Properties to dump more flexible.
     */
    public JsonItemWriter(Set<String> propertyNamesToIgnore) {
        this.propertyNamesToIgnore = propertyNamesToIgnore;
    }

    /** Dump all Nodes of given NodeIterator in JSON
     * @throws JSONException */
    public void dump(NodeIterator it, Writer out) throws RepositoryException, JSONException {
        final JSONWriter w = new JSONWriter(out);
        w.array();
        while (it.hasNext()) {
            dumpSingleNode(it.nextNode(), w, 1, 0);
        }
        w.endArray();
    }

    /** Dump given node in JSON, optionally recursing into its child nodes */
    public void dump(Node node, Writer w, int maxRecursionLevels) throws RepositoryException, JSONException {
        dump(node, new JSONWriter(w), 0, maxRecursionLevels);
    }

    /** Dump given property in JSON */
    public void dump(Property p, Writer w) throws JSONException, ValueFormatException, RepositoryException {
        final JSONWriter jw = new JSONWriter(w);
        jw.object();
        writeProperty(jw, p);
        jw.endObject();
    }

    /** Dump given node in JSON, optionally recursing into its child nodes */
    protected void dump(Node node, JSONWriter w, int currentRecursionLevel, int maxRecursionLevels)
    throws RepositoryException, JSONException {

        w.object();
        PropertyIterator props = node.getProperties();

        // the node's actual properties
        while (props.hasNext()) {
            Property prop = props.nextProperty();

            if (propertyNamesToIgnore!=null && propertyNamesToIgnore.contains(prop.getName())) {
                continue;
            }

            writeProperty(w, prop);
        }

        // the child nodes
        if(recursionLevelActive(currentRecursionLevel, maxRecursionLevels)) {
            final NodeIterator children = node.getNodes();
            while(children.hasNext()) {
                final Node n = children.nextNode();
                dumpSingleNode(n, w, currentRecursionLevel, maxRecursionLevels);
            }
        }

        w.endObject();
    }

    /** Dump a single node */
    protected void dumpSingleNode(Node n, JSONWriter w, int currentRecursionLevel, int maxRecursionLevels)
    throws RepositoryException, JSONException {
        if (recursionLevelActive(currentRecursionLevel, maxRecursionLevels)) {
            w.key(n.getName());
            dump(n, w, currentRecursionLevel + 1, maxRecursionLevels);
        }
    }

    /** true if the current recursion level is active */
    protected boolean recursionLevelActive(int currentRecursionLevel, int maxRecursionLevels) {
        return maxRecursionLevels < 0 || currentRecursionLevel < maxRecursionLevels;
    }

    /**
     * Write a single property
     */
    protected void writeProperty(JSONWriter w, Property p)
    throws ValueFormatException, RepositoryException, JSONException {
        // special handling for binaries: we dump the length and not the length
                if (p.getType() == PropertyType.BINARY) {
            // TODO for now we mark binary properties with an initial colon in their name
            // (colon is not allowed as a JCR property name)
            // in the name, and the value should be the size of the binary data
            w.key(":" + p.getName());
            if (!p.getDefinition().isMultiple()) {
                w.value(p.getLength());
            } else {
                final long[] sizes = p.getLengths();
                w.array();
                for(int i=0;i<sizes.length;i++) {
                    w.value(sizes[i]);
                }
                w.endArray();
            }
            return;
        }
        w.key(p.getName());

        if (!p.getDefinition().isMultiple()) {
            dumpValue(w, p.getValue());
        } else {
            w.key(p.getName());
            w.array();
            for(Value v : p.getValues()) {
                dumpValue(w, v);
            }
            w.endArray();
        }
    }

    /**
     * Writes the given value to the JSON writer. currently the following
     * conversions are done:
     * <table>
     *   <tr><th>JSR Property Type</th><th>JSON Value Type</th></tr>
     *   <tr><td>BINARY</td><td>always 0 as long</td></tr>
     *   <tr><td>DATE</td><td>converted date string as defined by ECMA</td></tr>
     *   <tr><td>BOOLEAN</td><td>boolean</td></tr>
     *   <tr><td>LONG</td><td>long</td></tr>
     *   <tr><td>DOUBLE</td><td>double</td></tr>
     *   <tr><td><i>all other</li></td><td>string</td></tr>
     * </table>
     * <sup>1</sup> Currently not implemented and uses 0 as default.
     *
     * @param w json writer
     * @param v value to dump
     */
    protected void dumpValue(JSONWriter w, Value v)
            throws ValueFormatException, IllegalStateException,
            RepositoryException, JSONException {

        switch (v.getType()) {
            case PropertyType.BINARY:
                w.value(0);
                break;

            case PropertyType.DATE:
                w.value(format(v.getDate()));
                break;

            case PropertyType.BOOLEAN:
                w.value(v.getBoolean());
                break;

            case PropertyType.LONG:
                w.value(v.getLong());
                break;

            case PropertyType.DOUBLE:
                w.value(v.getDouble());

                break;
            default:
                w.value(v.getString());
        }
    }

    public static synchronized String format(Calendar date) {
        if (calendarFormat == null) {
            calendarFormat = new SimpleDateFormat(
                "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", Locale.US);
        }
        return calendarFormat.format(date.getTime());
    }
}

@@ -5617,40 +5617,8 @@
 e %7B%0A
-            w.key(p.getName());%0A
     

Fix mistake introduced in Rev. 644750: Multivalued properties cause the key to be written twice, which causes a JSONException to be thrown.