001package gudusoft.gsqlparser.lineage.v2.model;
002
003import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
004import gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex;
005
006import java.math.BigInteger;
007import java.util.AbstractList;
008import java.util.AbstractMap;
009import java.util.ArrayDeque;
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.Deque;
013import java.util.Iterator;
014import java.util.LinkedHashMap;
015import java.util.List;
016import java.util.Map;
017import java.util.RandomAccess;
018import java.util.Set;
019import java.util.concurrent.ConcurrentHashMap;
020
021/**
022 * An object of a lineage document: its members as JSON values, with typed accessors in each generated subclass.
023 *
024 * <p><b>Values.</b> A member holds a JSON value: a {@code Map} with string keys, a {@code List}, a {@code String}, a
025 * {@code Long} (or a {@code BigInteger} outside the range of {@code long}), a {@code Boolean}, or JSON null. Integers
026 * given as {@code Integer}, {@code Short}, {@code Byte} or a {@code BigInteger} that fits are stored as {@code Long},
027 * so two objects with the same JSON are equal however they were built. Every member is kept, including members the
028 * model does not type (extensions, module content, members of a later minor version): reading a document and
029 * writing it again loses nothing.</p>
030 *
031 * <p><b>Absence and null.</b> A typed getter returns null when the member is absent and throws
032 * {@link IllegalStateException} when the member holds JSON of another type, JSON null included. A getter typed
033 * {@code Object} (any JSON value) returns {@link #JSON_NULL} for a member that is present with JSON null. A builder
034 * setter given null removes the member; given {@link #JSON_NULL} it sets JSON null.</p>
035 *
036 * <p><b>Immutability and cost.</b> Objects are immutable and thread-safe; a {@link Builder} makes a changed copy.
037 * Values are frozen once, without recursion, so any depth the strict reader accepts is supported. A typed getter
038 * computes its view once and returns the same object afterwards. Equality compares the canonical JSON
039 * (RFC 8785) of the members.</p>
040 *
041 * <p>The model checks shapes only when a typed accessor reads a member; it does not validate. Use
042 * {@link gudusoft.gsqlparser.lineage.v2.validation.LineageValidator} for that.</p>
043 *
044 * @since 4.2.17
045 */
046public abstract class SqllrObject {
047
048    /** JSON null as a member value, distinct from an absent member; see the class description. */
049    public static final Object JSON_NULL = new Object() {
050        @Override
051        public String toString() {
052            return "null";
053        }
054    };
055
056    /** Converts one JSON value to a typed value. */
057    public interface Reader<T> {
058        T read(Object json);
059    }
060
061    private static final Object NO_VALUE = new Object();
062
063    private final Map<String, Object> members;
064    private final ConcurrentHashMap<String, Object> views = new ConcurrentHashMap<String, Object>();
065    private volatile String canonical;
066
067    protected SqllrObject(Map<String, Object> members) {
068        this.members = members;
069    }
070
071    /** The members of a JSON object as frozen JSON values (a frozen tree is used as it is, not copied). */
072    @SuppressWarnings("unchecked")
073    protected static Map<String, Object> members(Object json, String type) {
074        if (json instanceof SqllrObject) {
075            return ((SqllrObject) json).members;
076        }
077        if (!(json instanceof Map<?, ?>)) {
078            throw new IllegalArgumentException(type + ": expected a JSON object, got " + describe(json));
079        }
080        return (Map<String, Object>) freeze(json);
081    }
082
083    private static String describe(Object json) {
084        return json == null || json == JSON_NULL ? "null" : json.getClass().getSimpleName();
085    }
086
087    /** The raw JSON value of a member: null when absent or JSON null (see {@link #has}). */
088    public final Object get(String member) {
089        return members.get(member);
090    }
091
092    public final boolean has(String member) {
093        return members.containsKey(member);
094    }
095
096    /** The members as a frozen JSON object, in the order they were set. */
097    public final Map<String, Object> toJson() {
098        return members;
099    }
100
101    private IllegalStateException wrongType(String member, String expected, Throwable cause) {
102        IllegalStateException e = new IllegalStateException(getClass().getSimpleName() + "." + member + " is not "
103                + expected + ": " + describe(members.get(member)) + (cause == null ? "" : " (" + cause.getMessage() + ")"));
104        if (cause != null) {
105            e.initCause(cause);
106        }
107        return e;
108    }
109
110    @SuppressWarnings("unchecked")
111    private <T> T view(String member, String expected, Reader<T> reader) {
112        Object cached = views.get(member);
113        if (cached != null) {
114            return cached == NO_VALUE ? null : (T) cached;
115        }
116        T value;
117        if (!members.containsKey(member)) {
118            value = null;
119        } else if (members.get(member) == null) {
120            throw wrongType(member, expected, null);
121        } else {
122            try {
123                value = reader.read(members.get(member));
124            } catch (IllegalArgumentException e) {
125                throw wrongType(member, expected, e);
126            } catch (IllegalStateException e) {
127                throw wrongType(member, expected, e);
128            }
129        }
130        Object previous = views.putIfAbsent(member, value == null ? NO_VALUE : value);
131        return previous == null || previous == NO_VALUE ? value : (T) previous;
132    }
133
134    protected final String string(String member) {
135        return view(member, "a string", STRING);
136    }
137
138    protected final Long integer(String member) {
139        return view(member, "an integer", INTEGER);
140    }
141
142    protected final Boolean bool(String member) {
143        return view(member, "a boolean", BOOLEAN);
144    }
145
146    protected final Map<String, Object> object(String member) {
147        return view(member, "an object", OBJECT);
148    }
149
150    /** Any JSON value: null when absent, {@link #JSON_NULL} when present with JSON null. */
151    protected final Object anyMember(String member) {
152        if (!members.containsKey(member)) {
153            return null;
154        }
155        Object value = members.get(member);
156        return value == null ? JSON_NULL : value;
157    }
158
159    protected final <T> T typed(String member, Reader<T> reader) {
160        return view(member, "an object of the expected shape", reader);
161    }
162
163    protected final <T> List<T> list(String member, Reader<T> reader) {
164        return view(member, "an array of the expected items", listOf(reader));
165    }
166
167    protected final <T> Map<String, T> map(String member, Reader<T> reader) {
168        return view(member, "an object of the expected members", mapOf(reader));
169    }
170
171    /** Reads a JSON array, each item with a reader, as an unmodifiable list. */
172    protected static <T> Reader<List<T>> listOf(final Reader<T> item) {
173        return new Reader<List<T>>() {
174            @Override
175            public List<T> read(Object json) {
176                if (!(json instanceof List<?>)) {
177                    throw new IllegalStateException("not an array: " + describe(json));
178                }
179                List<T> out = new ArrayList<T>();
180                int index = 0;
181                for (Object element : (List<?>) json) {
182                    if (element == null && item != ANY) {
183                        throw new IllegalStateException("item " + index + " is null");
184                    }
185                    try {
186                        out.add(item.read(element));
187                    } catch (IllegalArgumentException e) {
188                        throw new IllegalStateException("item " + index + ": " + e.getMessage(), e);
189                    }
190                    index++;
191                }
192                return Collections.unmodifiableList(out);
193            }
194        };
195    }
196
197    /** Reads a JSON object, each member with a reader, as an unmodifiable map. */
198    protected static <T> Reader<Map<String, T>> mapOf(final Reader<T> item) {
199        return new Reader<Map<String, T>>() {
200            @Override
201            public Map<String, T> read(Object json) {
202                if (!(json instanceof Map<?, ?>)) {
203                    throw new IllegalStateException("not an object: " + describe(json));
204                }
205                Map<String, T> out = new LinkedHashMap<String, T>();
206                for (Map.Entry<?, ?> entry : ((Map<?, ?>) json).entrySet()) {
207                    if (entry.getValue() == null && item != ANY) {
208                        throw new IllegalStateException("member " + entry.getKey() + " is null");
209                    }
210                    try {
211                        out.put((String) entry.getKey(), item.read(entry.getValue()));
212                    } catch (IllegalArgumentException e) {
213                        throw new IllegalStateException("member " + entry.getKey() + ": " + e.getMessage(), e);
214                    }
215                }
216                return Collections.unmodifiableMap(out);
217            }
218        };
219    }
220
221    /** Reads a JSON string. */
222    protected static final Reader<String> STRING = new Reader<String>() {
223        @Override
224        public String read(Object json) {
225            if (json instanceof String) {
226                return (String) json;
227            }
228            throw new IllegalStateException("not a string: " + describe(json));
229        }
230    };
231
232    /** Reads a JSON integer. */
233    protected static final Reader<Long> INTEGER = new Reader<Long>() {
234        @Override
235        public Long read(Object json) {
236            Long value = DocumentIndex.intRef(json);
237            if (value == null) {
238                throw new IllegalStateException("not an integer in the range of long: " + describe(json));
239            }
240            return value;
241        }
242    };
243
244    /** Reads a JSON boolean. */
245    protected static final Reader<Boolean> BOOLEAN = new Reader<Boolean>() {
246        @Override
247        public Boolean read(Object json) {
248            if (json instanceof Boolean) {
249                return (Boolean) json;
250            }
251            throw new IllegalStateException("not a boolean: " + describe(json));
252        }
253    };
254
255    /** Reads any JSON value as it is (JSON null as null). */
256    protected static final Reader<Object> ANY = new Reader<Object>() {
257        @Override
258        public Object read(Object json) {
259            return json;
260        }
261    };
262
263    /** Reads a JSON object as a map. */
264    protected static final Reader<Map<String, Object>> OBJECT = new Reader<Map<String, Object>>() {
265        @Override
266        @SuppressWarnings("unchecked")
267        public Map<String, Object> read(Object json) {
268            if (json instanceof Map<?, ?>) {
269                return (Map<String, Object>) json;
270            }
271            throw new IllegalStateException("not an object: " + describe(json));
272        }
273    };
274
275    // ---------------------------------------------------------------------------------------------
276    // Frozen JSON trees
277    // ---------------------------------------------------------------------------------------------
278
279    /** A frozen JSON object. */
280    private static final class FrozenMap extends AbstractMap<String, Object> {
281        private final Map<String, Object> map;
282
283        FrozenMap(LinkedHashMap<String, Object> map) {
284            this.map = Collections.unmodifiableMap(map);
285        }
286
287        @Override
288        public Set<Map.Entry<String, Object>> entrySet() {
289            return map.entrySet();
290        }
291
292        @Override
293        public Object get(Object key) {
294            return map.get(key);
295        }
296
297        @Override
298        public boolean containsKey(Object key) {
299            return map.containsKey(key);
300        }
301
302        @Override
303        public int size() {
304            return map.size();
305        }
306    }
307
308    /** A frozen JSON array. */
309    private static final class FrozenList extends AbstractList<Object> implements RandomAccess {
310        private final Object[] items;
311
312        FrozenList(List<Object> items) {
313            this.items = items.toArray();
314        }
315
316        @Override
317        public Object get(int index) {
318            return items[index];
319        }
320
321        @Override
322        public int size() {
323            return items.length;
324        }
325    }
326
327    private static boolean unfrozen(Object value) {
328        return (value instanceof Map<?, ?> && !(value instanceof FrozenMap))
329                || (value instanceof List<?> && !(value instanceof FrozenList));
330    }
331
332    /** A JSON scalar in its stored form, or a container to freeze, or a frozen tree to use as it is. */
333    private static Object normalize(Object value) {
334        if (value instanceof SqllrObject) {
335            return ((SqllrObject) value).members;
336        }
337        if (value == null || value == JSON_NULL) {
338            return null;
339        }
340        if (value instanceof Long || value instanceof String || value instanceof Boolean || value instanceof Map<?, ?>
341                || value instanceof List<?>) {
342            return value;
343        }
344        if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
345            return Long.valueOf(((Number) value).longValue());
346        }
347        if (value instanceof BigInteger) {
348            return ((BigInteger) value).bitLength() < 64 ? Long.valueOf(((BigInteger) value).longValue()) : value;
349        }
350        throw new IllegalArgumentException("not a JSON value of the model: " + value.getClass().getName());
351    }
352
353    /** One container being frozen. */
354    private static final class Frame {
355        final Iterator<?> items;
356        final LinkedHashMap<String, Object> map;
357        final List<Object> list;
358        String key;
359
360        Frame(Object container) {
361            if (container instanceof Map<?, ?>) {
362                items = ((Map<?, ?>) container).entrySet().iterator();
363                map = new LinkedHashMap<String, Object>();
364                list = null;
365            } else {
366                items = ((List<?>) container).iterator();
367                map = null;
368                list = new ArrayList<Object>();
369            }
370        }
371
372        /** The next value to freeze; for an object, its member name is kept until the value is added. */
373        Object next() {
374            Object item = items.next();
375            if (map != null) {
376                Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
377                if (!(entry.getKey() instanceof String)) {
378                    throw new IllegalArgumentException("member name " + entry.getKey() + " is not a string");
379                }
380                key = (String) entry.getKey();
381                return entry.getValue();
382            }
383            return item;
384        }
385
386        void add(Object value) {
387            if (map != null) {
388                map.put(key, value);
389            } else {
390                list.add(value);
391            }
392        }
393
394        Object build() {
395            return map != null ? new FrozenMap(map) : new FrozenList(list);
396        }
397    }
398
399    /** A frozen copy of a JSON value, made with an explicit stack; frozen subtrees are shared, not copied. */
400    static Object freeze(Object value) {
401        Object root = normalize(value);
402        if (!unfrozen(root)) {
403            return root;
404        }
405        Deque<Frame> stack = new ArrayDeque<Frame>();
406        stack.push(new Frame(root));
407        Object result = null;
408        while (!stack.isEmpty()) {
409            Frame frame = stack.peek();
410            if (frame.items.hasNext()) {
411                Object child = normalize(frame.next());
412                if (unfrozen(child)) {
413                    stack.push(new Frame(child));
414                } else {
415                    frame.add(child);
416                }
417            } else {
418                stack.pop();
419                Object frozen = frame.build();
420                if (stack.isEmpty()) {
421                    result = frozen;
422                } else {
423                    stack.peek().add(frozen);
424                }
425            }
426        }
427        return result;
428    }
429
430    /** The RFC 8785 text of the members, computed once. */
431    private String canonical() {
432        String text = canonical;
433        if (text == null) {
434            text = CanonicalJson.jcsText(members);
435            canonical = text;
436        }
437        return text;
438    }
439
440    @Override
441    public final boolean equals(Object other) {
442        return other != null && other.getClass() == getClass() && canonical().equals(((SqllrObject) other).canonical());
443    }
444
445    @Override
446    public final int hashCode() {
447        return canonical().hashCode();
448    }
449
450    /** The RFC 8785 text of the object. */
451    @Override
452    public final String toString() {
453        return canonical();
454    }
455
456    /**
457     * Collects the members of a new object. A setter given null removes the member; given {@link #JSON_NULL} it sets
458     * JSON null; a typed value, list or map of typed values is stored as its JSON.
459     */
460    public abstract static class Builder<T extends SqllrObject, B extends Builder<T, B>> {
461        private final LinkedHashMap<String, Object> members = new LinkedHashMap<String, Object>();
462
463        protected Builder(Map<String, Object> from) {
464            if (from != null) {
465                members.putAll(from);
466            }
467        }
468
469        @SuppressWarnings("unchecked")
470        private B self() {
471            return (B) this;
472        }
473
474        /** Sets a member the model may not type, such as module content; null removes it. */
475        public final B set(String member, Object value) {
476            if (value == null) {
477                members.remove(member);
478            } else {
479                members.put(member, value);
480            }
481            return self();
482        }
483
484        public final B remove(String member) {
485            members.remove(member);
486            return self();
487        }
488
489        /** The frozen members collected so far. */
490        @SuppressWarnings("unchecked")
491        protected final Map<String, Object> frozenMembers() {
492            return (Map<String, Object>) freeze(members);
493        }
494
495        public abstract T build();
496    }
497}