001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.schematic.document;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.EnumSet;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import org.modeshape.schematic.internal.document.BasicArray;
026import org.modeshape.schematic.internal.document.IndexSequence;
027import org.modeshape.schematic.internal.document.JsonReader;
028
029/**
030 * The constants pertaining to the JSON Schema (draft) specification.
031 * 
032 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
033 */
034public class JsonSchema {
035
036    public static class Version {
037        /**
038         * The version-related constants for the 3rd official draft of the specification.
039         * 
040         * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
041         */
042        public static class Draft3 {
043            /**
044             * The URL of the Core Meta-Schema.
045             */
046            public static final String CORE_METASCHEMA_URL = "http://json-schema.org/draft-03/schema#";
047        }
048
049        /**
050         * The version-related constants for the most recent version.
051         * 
052         * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
053         */
054        public static class Latest {
055            /**
056             * The URL of the Core Meta-Schema.
057             */
058            public static final String CORE_METASCHEMA_URL = Draft3.CORE_METASCHEMA_URL;
059        }
060    }
061
062    /**
063     * The enumeration representing the standard types used in the JSON Schema (draft) specification. Type.
064     * 
065     * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
066     */
067    public static enum Type {
068        STRING("string", new StringConverter()),
069        NUMBER("number", new NumberConverter()),
070        INTEGER("integer", new IntegerConverter()),
071        BOOLEAN("boolean", new BooleanConverter()),
072        OBJECT("object", new ObjectConverter()),
073        ARRAY("array", new ArrayConverter()),
074        NULL("null", new NullConverter()),
075        ANY("any", new AnyConverter()),
076        UNION("union", new UnionConverter()),
077        UNKNOWN("unknown", new UnknownConverter());
078
079        private static final Map<String, Type> TYPE_BY_LOWERCASE;
080        private static final Map<Type, EnumSet<Type>> EQUIVALENT_TYPES;
081        static {
082            Map<String, Type> typeByLowercase = new HashMap<String, JsonSchema.Type>();
083            typeByLowercase.put(STRING.toString().toLowerCase(), STRING);
084            typeByLowercase.put(NUMBER.toString().toLowerCase(), NUMBER);
085            typeByLowercase.put(INTEGER.toString().toLowerCase(), INTEGER);
086            typeByLowercase.put(BOOLEAN.toString().toLowerCase(), BOOLEAN);
087            typeByLowercase.put(OBJECT.toString().toLowerCase(), OBJECT);
088            typeByLowercase.put(ARRAY.toString().toLowerCase(), ARRAY);
089            typeByLowercase.put(NULL.toString().toLowerCase(), NULL);
090            typeByLowercase.put(ANY.toString().toLowerCase(), ANY);
091            typeByLowercase.put(UNION.toString().toLowerCase(), UNION);
092            typeByLowercase.put(UNKNOWN.toString().toLowerCase(), UNKNOWN);
093            TYPE_BY_LOWERCASE = Collections.unmodifiableMap(typeByLowercase);
094
095            Map<Type, EnumSet<Type>> equiv = new HashMap<Type, EnumSet<Type>>();
096            equiv.put(Type.STRING, EnumSet.of(Type.STRING));
097            equiv.put(Type.NUMBER, EnumSet.of(Type.NUMBER, Type.INTEGER));
098            equiv.put(Type.INTEGER, EnumSet.of(Type.INTEGER));
099            equiv.put(Type.BOOLEAN, EnumSet.of(Type.BOOLEAN));
100            equiv.put(Type.OBJECT, EnumSet.of(Type.OBJECT));
101            equiv.put(Type.ARRAY, EnumSet.of(Type.ARRAY));
102            equiv.put(Type.NULL, EnumSet.of(Type.NULL));
103            equiv.put(Type.ANY, EnumSet.of(Type.STRING,
104                                           Type.NUMBER,
105                                           Type.INTEGER,
106                                           Type.BOOLEAN,
107                                           Type.OBJECT,
108                                           Type.ARRAY,
109                                           Type.NULL,
110                                           Type.ANY,
111                                           Type.UNION,
112                                           Type.UNKNOWN));
113            equiv.put(Type.UNION, EnumSet.of(Type.UNKNOWN));
114            equiv.put(Type.UNKNOWN, EnumSet.of(Type.UNKNOWN));
115            EQUIVALENT_TYPES = Collections.unmodifiableMap(equiv);
116        }
117
118        private final String type;
119        private final ValueConverter<?> converter;
120
121        private Type( String type,
122                      ValueConverter<?> converter ) {
123            this.type = type;
124            this.converter = converter;
125        }
126
127        @Override
128        public String toString() {
129            return type;
130        }
131
132        /**
133         * Find the type enumeration literal given the (case-insensitive) name.
134         * 
135         * @param name the case-independent name of the type
136         * @return the type, or null if there is no such type
137         */
138        public static Type byName( String name ) {
139            return TYPE_BY_LOWERCASE.get(name.toLowerCase());
140        }
141
142        /**
143         * Determine whether this type is equivalent to the supplied type.
144         * 
145         * @param other the type to be compared with this type
146         * @return true if the types are equivalent, or false otherwise
147         */
148        public boolean isEquivalent( Type other ) {
149            return other != null ? EQUIVALENT_TYPES.get(this).contains(other) : false;
150        }
151
152        /**
153         * Attempt to convert the supplied value (with the given type) into a value compatible with this type.
154         * 
155         * @param actualValue the value to be converted; may be null
156         * @param actualType the type of the value; may be null
157         * @return the converted value, or null if the value could not be converted to this type
158         */
159        public Object convertValueFrom( Object actualValue,
160                                        Type actualType ) {
161            if (actualType == null) actualType = Type.typeFor(actualValue);
162            return converter.convert(actualValue, actualType);
163        }
164
165        /**
166         * Determine the type for the given value.
167         * 
168         * @param value the field value
169         * @return the corresponding type for the value; never null
170         */
171        public static Type typeFor( Object value ) {
172            if (value == null) return NULL;
173            if (value instanceof String) // most will be strings
174            return STRING;
175            if (value instanceof Integer) return INTEGER;
176            if (value instanceof Long) return INTEGER;
177            if (value instanceof Float) return NUMBER;
178            if (value instanceof Double) return NUMBER;
179            if (value instanceof Boolean) return BOOLEAN;
180            if (value instanceof List) // needs to be compared **before** Document
181            return ARRAY;
182            if (value instanceof Document) return OBJECT;
183            if (value instanceof Null) return NULL;
184            if (value instanceof Symbol) return STRING;
185            return UNKNOWN;
186        }
187
188        /**
189         * Obtain the set of types given the supplied name or iterable container of names.
190         * 
191         * @param typeName the String or Symbol representation of a single type name, or an Iterable&amp;?> list of names
192         * @return the set of types that correspond to the supplied names; never null but possibly an empty set
193         */
194        public static EnumSet<Type> typesWithNames( Object typeName ) {
195            if (typeName instanceof List) {
196                List<Type> result = new ArrayList<Type>();
197                Iterable<?> typeNames = (Iterable<?>)typeName;
198                for (Object tname : typeNames) {
199                    if (tname == null) continue;
200                    String name = tname.toString();
201                    Type type = byName(name);
202                    if (type != null) result.add(type);
203                }
204                if (!result.isEmpty()) {
205                    return EnumSet.copyOf(result);
206                }
207            } else if (typeName instanceof String || typeName instanceof Symbol) {
208                String name = typeName.toString();
209                Type type = byName(name);
210                return EnumSet.of(type);
211            }
212            return EnumSet.noneOf(Type.class);
213        }
214    }
215
216    protected static interface ValueConverter<OutputType> {
217        OutputType convert( Object value,
218                            Type type );
219    }
220
221    protected static class StringConverter implements ValueConverter<String> {
222        @Override
223        public String convert( Object value,
224                               Type type ) {
225            if (value == null) return null;
226            switch (type) {
227                case STRING:
228                    return (String)value;
229                case INTEGER:
230                case NUMBER:
231                case BOOLEAN:
232                    return value.toString();
233                case OBJECT: // this is a document, and we can't convert it to a string ...
234                case UNION:
235                case NULL:
236                case ARRAY:
237                    return null;
238                case ANY: // fall through
239                case UNKNOWN:
240                    // Figure out the type ...
241                    Type inferredType = Type.typeFor(value);
242                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
243                    return convert(value, inferredType);
244            }
245            return null;
246        }
247    }
248
249    protected static class NumberConverter implements ValueConverter<Number> {
250        @Override
251        public Number convert( Object value,
252                               Type type ) {
253            if (value == null) return null;
254            switch (type) {
255                case STRING:
256                    String str = value.toString();
257                    return JsonReader.parseNumber(str); // may return null
258                case INTEGER:
259                case NUMBER:
260                    return (Number)value;
261                case BOOLEAN:
262                    // Convert a boolean to either '1' or '0' integer ...
263                    Boolean bool = (Boolean)value;
264                    return Boolean.TRUE.equals(bool) ? new Integer(1) : new Integer(0);
265                case OBJECT: // this is a document, and we can't convert it to a number ...
266                case UNION:
267                case NULL:
268                case ARRAY:
269                    return null;
270                case ANY: // fall through
271                case UNKNOWN:
272                    // Figure out the type ...
273                    Type inferredType = Type.typeFor(value);
274                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
275                    return convert(value, inferredType);
276            }
277            return null;
278        }
279    }
280
281    protected static class IntegerConverter implements ValueConverter<Integer> {
282        @Override
283        public Integer convert( Object value,
284                                Type type ) {
285            if (value == null) return null;
286            switch (type) {
287                case STRING:
288                    String str = value.toString();
289                    Number number = JsonReader.parseNumber(str); // may return null
290                    return number instanceof Integer ? (Integer)number : null;
291                case INTEGER:
292                    return (Integer)value;
293                case NUMBER:
294                    return value instanceof Integer ? (Integer)value : null;
295                case BOOLEAN:
296                    // Convert a boolean to either '1' or '0' integer ...
297                    Boolean bool = (Boolean)value;
298                    return Boolean.TRUE.equals(bool) ? new Integer(1) : new Integer(0);
299                case OBJECT: // this is a document, and we can't convert it to a number ...
300                case UNION:
301                case NULL:
302                case ARRAY:
303                    return null;
304                case ANY: // fall through
305                case UNKNOWN:
306                    // Figure out the type ...
307                    Type inferredType = Type.typeFor(value);
308                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
309                    return convert(value, inferredType);
310            }
311            return null;
312        }
313    }
314
315    protected static class BooleanConverter implements ValueConverter<Boolean> {
316        @Override
317        public Boolean convert( Object value,
318                                Type type ) {
319            if (value == null) return null;
320            switch (type) {
321                case STRING:
322                    String str = value.toString();
323                    if ("true".equalsIgnoreCase(str)) return Boolean.TRUE;
324                    if ("false".equalsIgnoreCase(str)) return Boolean.FALSE;
325                    return null;
326                case INTEGER:
327                    Integer i = (Integer)value;
328                    if (i.intValue() == 1) return Boolean.FALSE;
329                    if (i.intValue() == 0) return Boolean.FALSE;
330                    return null;
331                case NUMBER:
332                    Number number = (Number)value;
333                    if (number.intValue() == 1) return Boolean.FALSE;
334                    if (number.intValue() == 0) return Boolean.FALSE;
335                    return null;
336                case BOOLEAN:
337                    return (Boolean)value;
338                case OBJECT: // this is a document, and we can't convert it to a number ...
339                case UNION:
340                case NULL:
341                case ARRAY:
342                    return null;
343                case ANY: // fall through
344                case UNKNOWN:
345                    // Figure out the type ...
346                    Type inferredType = Type.typeFor(value);
347                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
348                    return convert(value, inferredType);
349            }
350            return null;
351        }
352    }
353
354    protected static class ArrayConverter implements ValueConverter<List<?>> {
355        @Override
356        public List<?> convert( Object value,
357                                Type type ) {
358            if (value == null) return null;
359            switch (type) {
360                case ARRAY:
361                    return value instanceof List<?> ? (List<?>)value : null;
362                case OBJECT: // this is a document
363                    if (value instanceof List<?>) return (List<?>)value;
364                    Document doc = (Document)value;
365                    BasicArray array = new BasicArray(doc.size());
366                    final Iterator<String> indexIter = IndexSequence.infiniteSequence();
367                    for (Document.Field field : doc.fields()) {
368                        String name = field.getName();
369                        String index = indexIter.next();
370                        if (!index.equals(name)) {
371                            // The doc's field names don't match integral index values ...
372                            return null;
373                        }
374                        array.addValue(value);
375                    }
376                    return array;
377                case STRING:
378                case INTEGER:
379                case NUMBER:
380                case BOOLEAN:
381                case UNION:
382                case NULL:
383                    return null;
384                case ANY: // fall through
385                case UNKNOWN:
386                    // Figure out the type ...
387                    Type inferredType = Type.typeFor(value);
388                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
389                    return convert(value, inferredType);
390            }
391            return null;
392        }
393    }
394
395    protected static class ObjectConverter implements ValueConverter<Object> {
396        @Override
397        public Object convert( Object value,
398                               Type type ) {
399            if (value == null) return null;
400            switch (type) {
401                case OBJECT: // this is a document
402                    return value;
403                case STRING:
404                case INTEGER:
405                case NUMBER:
406                case BOOLEAN:
407                case UNION:
408                case NULL:
409                    return null;
410                case ARRAY:
411                    // The array can already be treated as a document ...
412                    return value instanceof Document ? value : null;
413                case ANY: // fall through
414                case UNKNOWN:
415                    // Figure out the type ...
416                    Type inferredType = Type.typeFor(value);
417                    if (inferredType == Type.UNKNOWN || inferredType == type) return null;
418                    return convert(value, inferredType);
419            }
420            return null;
421        }
422    }
423
424    protected static class NullConverter implements ValueConverter<Object> {
425        @Override
426        public Object convert( Object value,
427                               Type type ) {
428            if (value == null || value instanceof Null) return Null.getInstance();
429            return null;
430        }
431    }
432
433    protected static class UnionConverter implements ValueConverter<Object> {
434        @Override
435        public Object convert( Object value,
436                               Type type ) {
437            return null;
438        }
439    }
440
441    protected static class UnknownConverter implements ValueConverter<Object> {
442        @Override
443        public Object convert( Object value,
444                               Type type ) {
445            return value;
446        }
447    }
448
449    protected static class AnyConverter implements ValueConverter<Object> {
450        @Override
451        public Object convert( Object value,
452                               Type type ) {
453            return value;
454        }
455    }
456
457}