/**
 * 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.jena.atlas.json ;

import java.math.BigDecimal ;
import java.util.ArrayDeque ;
import java.util.Deque ;
import java.util.Objects;

import org.apache.jena.atlas.logging.Log ;

/* Builder pattern for JSON.
 * The JsonValue built can be an array or object at the outmost leve, but not a atomic value.  
 */
public class JsonBuilder {

    // If not an array or object.
    private JsonValue         builtValue    = null ;
    private static final String NoMarker    = "" ;
    private Deque<String>     markers       = new ArrayDeque<>() ;
    private Deque<JsonArray>  arrays        = new ArrayDeque<>() ;
    private Deque<JsonObject> objects       = new ArrayDeque<>() ;

    private static enum State {
        ARRAY, OBJECT
    }
    private Deque<State>  stack = new ArrayDeque<>() ;

    // The depth of this stack is the object depth. key: { key: ... } 
    private Deque<String> keys  = new ArrayDeque<>() ;

    public static JsonBuilder create() { return new JsonBuilder() ; }
    
    public JsonBuilder() {

    }

    public JsonValue build() {
        if ( builtValue == null ) {
            if ( objects.isEmpty() && arrays.isEmpty() )
                throw new JsonException("Alignment error: no object or array started") ;
            throw new JsonException("Alignment error: unfinished outer object or array") ;
        }
        return builtValue ;
    }
    
    public void reset() {
        builtValue = null ;
        stack.clear() ;
        objects.clear() ;
        keys.clear();
        arrays.clear(); 
    }

    public JsonBuilder startObject() { return startObject(NoMarker) ; }
    
    public JsonBuilder startObject(String startMarker) {
        markers.push(startMarker); 
        objects.push(new JsonObject()) ;
        stack.push(State.OBJECT) ;
        return this ;
    }

    public JsonBuilder finishObject() { return finishObject(NoMarker) ; }

    public JsonBuilder finishObject(String finishMarker) {
        if ( stack.isEmpty() )
            throw new JsonException("Alignment error : already built outer-most object or array") ; 
        State state = stack.pop() ;
        if ( state != State.OBJECT )
            throw new JsonException("JSON build error : not in an object") ;
        JsonValue value = objects.pop() ;
        maybeObjectOrArray(value) ;
        if ( stack.isEmpty() )
            builtValue = value ;
        String startMarker = markers.pop(); 
        if ( ! Objects.equals(startMarker, finishMarker) )
            throw new JsonException("JSON build error : start/finish alignment error: start="+startMarker+"  finish="+finishMarker) ;
        return this ;
    }

    public JsonBuilder startArray() {
        arrays.push(new JsonArray()) ;
        stack.push(State.ARRAY) ;
        return this ;
    }

    public JsonBuilder finishArray() {
        if ( stack.isEmpty() )
            throw new JsonException("Alignment error : already built outer-most object or array") ; 

        State state = stack.pop() ;
        if ( state != State.ARRAY )
            throw new JsonException("JSON build error : not in an array") ;

        JsonValue value = arrays.pop() ;
        maybeObjectOrArray(value) ;
        if ( stack.isEmpty() )
            builtValue = value ;
        return this ;
    }

    public JsonBuilder key(String key) {
        State state = stack.peek() ;
        if ( state != State.OBJECT )
            throw new JsonException("JSON build error : not in an object") ;
        keys.push(key) ;
        return this ;
    }

    private void maybeObjectOrArray(JsonValue value) {
        if ( stack.size() == 0 )
            // Error.
            return ;

        switch (stack.peek()) {
            case OBJECT : {
                String k = keys.pop() ;
                JsonObject obj = objects.peek() ;
                if ( obj.hasKey(k) )
                    Log.warn(this, "Duplicate key '" + k + "' for object") ;
                obj.put(k, value) ;
                return ;
            }
            case ARRAY : {
                arrays.peek().add(value) ;
                return ;
            }
        }
    }

    public JsonBuilder value(JsonValue v) {
        maybeObjectOrArray(v) ;
        return this ;
    }
    
    public JsonBuilder value(boolean b) {
        JsonValue value = new JsonBoolean(b) ;
        maybeObjectOrArray(value) ;
        return this ;
    }

    public JsonBuilder value(BigDecimal decimal) {
        JsonValue value = JsonNumber.value(decimal) ;
        maybeObjectOrArray(value) ;
        return this ;
    }

    public JsonBuilder value(double d) {
        JsonValue value = JsonNumber.value(d) ;
        maybeObjectOrArray(value) ;
        return this ;
    }

    public JsonBuilder value(long val) {
        JsonValue value = JsonNumber.value(val) ;
        maybeObjectOrArray(value) ;
        return this ;
    }

    public JsonBuilder valueNull() {
        JsonValue value = JsonNull.instance ;
        maybeObjectOrArray(value) ;
        return this ;
    }

    public JsonBuilder value(String string) {
        JsonValue value = new JsonString(string) ;
        maybeObjectOrArray(value) ;
        return this ;
    }
}

@@ -4079,32 +4079,788 @@
 n this ;%0A    %7D%0A%0A
+    public JsonBuilder pair(String key, JsonValue value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A    public JsonBuilder pair(String key, boolean value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A    public JsonBuilder pair(String key, BigDecimal value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A    public JsonBuilder pair(String key, double value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A    public JsonBuilder pair(String key, long value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A    public JsonBuilder pair(String key, String value) %7B%0A        key(key);%0A        value(value);%0A        return this ;%0A    %7D%0A%0A
     public JsonB

Add pair(,)