001package gudusoft.gsqlparser.slpc.model;
002
003import java.math.BigInteger;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.LinkedHashMap;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010
011/**
012 * Immutable JSON object used by the SLPC reference model.
013 *
014 * <p>The contract intentionally distinguishes an absent member from a member
015 * whose value is {@code null}.  A generic immutable tree keeps that distinction
016 * and also lets the Java model follow the versioned JSON Schema without a
017 * second, drifting set of 150+ mutable DTO classes.</p>
018 */
019public final class SlpcObject {
020
021    private final Map<String, Object> values;
022
023    private SlpcObject(Map<String, Object> source) {
024        LinkedHashMap<String, Object> copy = new LinkedHashMap<String, Object>();
025        for (Map.Entry<String, Object> entry : source.entrySet()) {
026            if (entry.getKey() == null) {
027                throw new IllegalArgumentException("SLPC object member name must not be null");
028            }
029            copy.put(entry.getKey(), freeze(entry.getValue()));
030        }
031        this.values = Collections.unmodifiableMap(copy);
032    }
033
034    public static Builder builder() {
035        return new Builder();
036    }
037
038    public boolean contains(String name) {
039        return values.containsKey(name);
040    }
041
042    public Object get(String name) {
043        return values.get(name);
044    }
045
046    public String string(String name) {
047        Object value = values.get(name);
048        return value instanceof String ? (String) value : null;
049    }
050
051    public BigInteger integer(String name) {
052        Object value = values.get(name);
053        return value instanceof BigInteger ? (BigInteger) value : null;
054    }
055
056    public SlpcObject object(String name) {
057        Object value = values.get(name);
058        return value instanceof SlpcObject ? (SlpcObject) value : null;
059    }
060
061    public List<Object> array(String name) {
062        Object value = values.get(name);
063        if (!(value instanceof List<?>)) {
064            return null;
065        }
066        @SuppressWarnings("unchecked")
067        List<Object> result = (List<Object>) value;
068        return result;
069    }
070
071    public Set<String> names() {
072        return values.keySet();
073    }
074
075    public Map<String, Object> asMap() {
076        return values;
077    }
078
079    public Builder toBuilder() {
080        Builder builder = new Builder();
081        builder.values.putAll(values);
082        return builder;
083    }
084
085    @Override
086    public boolean equals(Object other) {
087        return other instanceof SlpcObject && values.equals(((SlpcObject) other).values);
088    }
089
090    @Override
091    public int hashCode() {
092        return values.hashCode();
093    }
094
095    private static Object freeze(Object value) {
096        if (value == null || value instanceof String || value instanceof Boolean
097                || value instanceof BigInteger || value instanceof SlpcObject) {
098            return value;
099        }
100        if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) {
101            return BigInteger.valueOf(((Number) value).longValue());
102        }
103        if (value instanceof Map<?, ?>) {
104            Builder builder = builder();
105            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
106                if (!(entry.getKey() instanceof String)) {
107                    throw new IllegalArgumentException("SLPC object member name must be a string");
108                }
109                builder.put((String) entry.getKey(), entry.getValue());
110            }
111            return builder.build();
112        }
113        if (value instanceof Iterable<?>) {
114            ArrayList<Object> copy = new ArrayList<Object>();
115            for (Object item : (Iterable<?>) value) {
116                copy.add(freeze(item));
117            }
118            return Collections.unmodifiableList(copy);
119        }
120        throw new IllegalArgumentException("Unsupported SLPC value type: " + value.getClass().getName());
121    }
122
123    public static final class Builder {
124        private final LinkedHashMap<String, Object> values = new LinkedHashMap<String, Object>();
125
126        public Builder put(String name, Object value) {
127            if (name == null) {
128                throw new IllegalArgumentException("SLPC object member name must not be null");
129            }
130            values.put(name, value);
131            return this;
132        }
133
134        /** Adds an explicitly present JSON null. */
135        public Builder putNull(String name) {
136            return put(name, null);
137        }
138
139        /** Adds the member only when the value is non-null. */
140        public Builder putIfPresent(String name, Object value) {
141            if (value != null) {
142                put(name, value);
143            }
144            return this;
145        }
146
147        public Builder remove(String name) {
148            values.remove(name);
149            return this;
150        }
151
152        public SlpcObject build() {
153            return new SlpcObject(values);
154        }
155    }
156}