001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019package org.apache.isis.viewer.restfulobjects.rendering.domainobjects;
020
021import java.math.BigDecimal;
022import java.math.BigInteger;
023import java.util.Arrays;
024import java.util.Date;
025import java.util.List;
026import java.util.Map;
027
028import com.google.common.base.Function;
029import com.google.common.collect.Iterables;
030import com.google.common.collect.Lists;
031import com.google.common.collect.Maps;
032
033import org.codehaus.jackson.node.NullNode;
034import org.joda.time.LocalDate;
035import org.joda.time.LocalDateTime;
036import org.joda.time.format.DateTimeFormat;
037import org.joda.time.format.DateTimeFormatter;
038
039import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
040import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
041import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
042import org.apache.isis.core.metamodel.spec.ObjectSpecId;
043import org.apache.isis.core.metamodel.spec.ObjectSpecification;
044import org.apache.isis.core.runtime.system.context.IsisContext;
045import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
046
047/**
048 * Similar to Isis' value encoding, but with additional support for JSON
049 * primitives.
050 */
051public final class JsonValueEncoder {
052
053    private JsonValueEncoder(){}
054
055    
056    public static class ExpectedStringRepresentingValueException extends IllegalArgumentException {
057        private static final long serialVersionUID = 1L;
058    }
059
060    public static abstract class JsonValueConverter {
061
062        protected final String format;
063        protected final String xIsisFormat;
064        private final Class<?>[] classes;
065
066        public JsonValueConverter(String format, String xIsisFormat, Class<?>... classes) {
067            this.format = format;
068            this.xIsisFormat = xIsisFormat;
069            this.classes = classes;
070        }
071
072        public List<ObjectSpecId> getSpecIds() {
073            return Lists.newArrayList(Iterables.transform(Arrays.asList(classes), new Function<Class<?>, ObjectSpecId>() {
074                public ObjectSpecId apply(Class<?> cls) {
075                    return new ObjectSpecId(cls.getName());
076                }
077            }));
078        }
079        
080        /**
081         * The value, otherwise <tt>null</tt>.
082         */
083        public abstract ObjectAdapter asAdapter(JsonRepresentation repr);
084        
085        public void appendValueAndFormat(ObjectAdapter objectAdapter, JsonRepresentation repr) {
086            append(repr, objectAdapter, format, xIsisFormat);
087        }
088
089        public Object asObject(ObjectAdapter objectAdapter) {
090            return objectAdapter.getObject();
091        }
092    }
093    
094    private static Map<ObjectSpecId, JsonValueConverter> converterBySpec = Maps.newLinkedHashMap();
095    
096    private static void putConverter(JsonValueConverter jvc) {
097        final List<ObjectSpecId> specIds = jvc.getSpecIds();
098        for (ObjectSpecId specId : specIds) {
099            converterBySpec.put(specId, jvc);
100        }
101    }
102
103    static {
104        putConverter(new JsonValueConverter(null, "boolean", boolean.class, Boolean.class){
105            @Override
106            public ObjectAdapter asAdapter(JsonRepresentation repr) {
107                if (repr.isBoolean()) {
108                    return adapterFor(repr.asBoolean());
109                } 
110                return null;
111            }
112        });
113        
114        putConverter(new JsonValueConverter(null, "byte", byte.class, Byte.class){
115            @Override
116            public ObjectAdapter asAdapter(JsonRepresentation repr) {
117                if (repr.isNumber()) {
118                    return adapterFor(repr.asNumber().byteValue());
119                }
120                if (repr.isInt()) {
121                    return adapterFor((byte)(int)repr.asInt());
122                }
123                if (repr.isLong()) {
124                    return adapterFor((byte)(long)repr.asLong());
125                }
126                if (repr.isBigInteger()) {
127                    return adapterFor(repr.asBigInteger().byteValue());
128                }
129                return null;
130            }
131        });
132        
133        putConverter(new JsonValueConverter(null, "short", short.class, Short.class){
134            @Override
135            public ObjectAdapter asAdapter(JsonRepresentation repr) {
136                if (repr.isNumber()) {
137                    return adapterFor(repr.asNumber().shortValue());
138                }
139                if (repr.isInt()) {
140                    return adapterFor((short)(int)repr.asInt());
141                }
142                if (repr.isLong()) {
143                    return adapterFor((short)(long)repr.asLong());
144                }
145                if (repr.isBigInteger()) {
146                    return adapterFor(repr.asBigInteger().shortValue());
147                }
148                return null;
149            }
150        });
151        
152        putConverter(new JsonValueConverter("int", "int", int.class, Integer.class){
153            @Override
154            public ObjectAdapter asAdapter(JsonRepresentation repr) {
155                if (repr.isInt()) {
156                    return adapterFor(repr.asInt());
157                }
158                if (repr.isLong()) {
159                    return adapterFor((int)(long)repr.asLong());
160                }
161                if (repr.isBigInteger()) {
162                    return adapterFor(repr.asBigInteger().intValue());
163                }
164                if (repr.isNumber()) {
165                    return adapterFor(repr.asNumber().intValue());
166                }
167                return null;
168            }
169        });
170        
171        putConverter(new JsonValueConverter("int", "long", long.class, Long.class){
172            @Override
173            public ObjectAdapter asAdapter(JsonRepresentation repr) {
174                if (repr.isLong()) {
175                    return adapterFor(repr.asLong());
176                }
177                if (repr.isInt()) {
178                    return adapterFor(repr.asInt());
179                }
180                if (repr.isBigInteger()) {
181                    return adapterFor(repr.asBigInteger().longValue());
182                }
183                if (repr.isNumber()) {
184                    return adapterFor(repr.asNumber().longValue());
185                }
186                return null;
187            }
188        });
189        
190        putConverter(new JsonValueConverter("decimal", "float", float.class, Float.class){
191            @Override
192            public ObjectAdapter asAdapter(JsonRepresentation repr) {
193                if (repr.isDouble()) {
194                    return adapterFor((float)(double)repr.asDouble());
195                }
196                if (repr.isNumber()) {
197                    return adapterFor(repr.asNumber().floatValue());
198                }
199                if (repr.isLong()) {
200                    return adapterFor((float)repr.asLong());
201                }
202                if (repr.isInt()) {
203                    return adapterFor((float)repr.asInt());
204                }
205                if (repr.isBigInteger()) {
206                    return adapterFor(repr.asBigInteger().floatValue());
207                }
208                return null;
209            }
210        });
211        
212        putConverter(new JsonValueConverter("decimal", "double", double.class, Double.class){
213            @Override
214            public ObjectAdapter asAdapter(JsonRepresentation repr) {
215                if (repr.isDouble()) {
216                    return adapterFor(repr.asDouble());
217                }
218                if (repr.isLong()) {
219                    return adapterFor((double)repr.asLong());
220                }
221                if (repr.isInt()) {
222                    return adapterFor((double)repr.asInt());
223                }
224                if (repr.isBigInteger()) {
225                    return adapterFor(repr.asBigInteger().doubleValue());
226                }
227                if (repr.isBigDecimal()) {
228                    return adapterFor(repr.asBigDecimal().doubleValue());
229                }
230                if (repr.isNumber()) {
231                    return adapterFor(repr.asNumber().doubleValue());
232                }
233                return null;
234            }
235        });
236        
237        putConverter(new JsonValueConverter(null, "char", char.class, Character.class){
238            @Override
239            public ObjectAdapter asAdapter(JsonRepresentation repr) {
240                if (repr.isString()) {
241                    final String str = repr.asString();
242                    if(str != null && str.length()>0) {
243                        return adapterFor(str.charAt(0));
244                    }
245                }
246                return null;
247            }
248        });
249        
250        putConverter(new JsonValueConverter("int", "biginteger", BigInteger.class){
251            @Override
252            public ObjectAdapter asAdapter(JsonRepresentation repr) {
253                if (repr.isBigInteger()) {
254                    return adapterFor(repr.asBigInteger());
255                }
256                if (repr.isLong()) {
257                    return adapterFor(BigInteger.valueOf(repr.asLong()));
258                }
259                if (repr.isInt()) {
260                    return adapterFor(BigInteger.valueOf(repr.asInt()));
261                }
262                if (repr.isNumber()) {
263                    return adapterFor(BigInteger.valueOf(repr.asNumber().longValue()));
264                }
265                return null;
266            }
267        });
268        
269        putConverter(new JsonValueConverter("decimal", "bigdecimal", BigDecimal.class){
270            @Override
271            public ObjectAdapter asAdapter(JsonRepresentation repr) {
272                // TODO: if inferring a BigDecimal, need to get the scale from somewhere...
273                if (repr.isBigDecimal()) {
274                    return adapterFor(repr.asBigDecimal());
275                }
276                if (repr.isBigInteger()) {
277                    return adapterFor(new BigDecimal(repr.asBigInteger()));
278                }
279                if (repr.isDouble()) {
280                    return adapterFor(BigDecimal.valueOf(repr.asDouble()));
281                }
282                if (repr.isLong()) {
283                    return adapterFor(BigDecimal.valueOf(repr.asLong()));
284                }
285                if (repr.isInt()) {
286                    return adapterFor(BigDecimal.valueOf(repr.asInt()));
287                }
288                return null;
289            }
290        });
291
292        putConverter(new JsonValueConverter("date", "jodalocaldate", LocalDate.class){
293
294            final DateTimeFormatter yyyyMMdd = JsonRepresentation.yyyyMMdd;
295
296            @Override
297            public ObjectAdapter asAdapter(JsonRepresentation repr) {
298                if (repr.isString()) {
299                    final String dateStr = repr.asString();
300                    try {
301                        final Date parsedDate = yyyyMMdd.parseDateTime(dateStr).toDate();
302                        return adapterFor(parsedDate);
303                    } catch (IllegalArgumentException ex) {
304                        // fall through
305                    }
306                }
307                return null;
308            }
309
310            @Override
311            public void appendValueAndFormat(ObjectAdapter objectAdapter, JsonRepresentation repr) {
312                final LocalDate date = (LocalDate) unwrap(objectAdapter);
313                final String dateStr = yyyyMMdd.print(date.toDateTimeAtStartOfDay());
314                append(repr, dateStr, format, xIsisFormat);
315            }
316        });
317
318        putConverter(new JsonValueConverter("date-time", "jodalocaldatetime", LocalDateTime.class){
319            final DateTimeFormatter yyyyMMddHHmmss = JsonRepresentation.yyyyMMddTHHmmssZ;
320            @Override
321            public ObjectAdapter asAdapter(JsonRepresentation repr) {
322                if (repr.isString()) {
323                    final String dateStr = repr.asString();
324                    try {
325                        final Date parsedDate = yyyyMMddHHmmss.parseDateTime(dateStr).toDate();
326                        return adapterFor(parsedDate);
327                    } catch (IllegalArgumentException ex) {
328                        // fall through
329                    }
330                }
331                return null;
332            }
333    
334            @Override
335            public void appendValueAndFormat(ObjectAdapter objectAdapter, JsonRepresentation repr) {
336                final LocalDateTime date = (LocalDateTime) unwrap(objectAdapter);
337                final String dateStr = yyyyMMddHHmmss.print(date.toDateTime());
338                append(repr, dateStr, format, xIsisFormat);
339            }
340        });
341        
342    }
343
344
345
346    public static ObjectAdapter asAdapter(final ObjectSpecification objectSpec, final JsonRepresentation argRepr) {
347        if (objectSpec == null) {
348            String reason = "ObjectSpec is null, cannot validate";
349            argRepr.mapPut("invalidReason", reason);
350            throw new IllegalArgumentException(reason);
351        }
352        final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
353        if (encodableFacet == null) {
354            String reason = "ObjectSpec expected to have an EncodableFacet";
355            argRepr.mapPut("invalidReason", reason);
356            throw new IllegalArgumentException(reason);
357        }
358        
359        if(!argRepr.mapHas("value")) {
360            String reason = "No 'value' key";
361            argRepr.mapPut("invalidReason", reason);
362            throw new IllegalArgumentException(reason);
363        }
364        final JsonRepresentation argValueRepr = argRepr.getRepresentation("value");
365        if(argValueRepr == null) {
366            return null;
367        }
368        if (!argValueRepr.isValue()) {
369            String reason = "Representation must be of a value";
370            argRepr.mapPut("invalidReason", reason);
371            throw new IllegalArgumentException(reason);
372        }
373
374        final JsonValueConverter jvc = converterBySpec.get(objectSpec.getSpecId());
375        if(jvc == null) {
376            // best effort
377            if (argValueRepr.isString()) {
378                final String argStr = argValueRepr.asString();
379                return encodableFacet.fromEncodedString(argStr);
380            }
381
382            final String reason = "Unable to parse value";
383            argRepr.mapPut("invalidReason", reason);
384            throw new IllegalArgumentException(reason);
385        }
386
387        final ObjectAdapter asAdapter = jvc.asAdapter(argValueRepr);
388        if(asAdapter != null) {
389            return asAdapter;
390        }
391        
392        // last attempt
393        if (argValueRepr.isString()) {
394            final String argStr = argValueRepr.asString();
395            return encodableFacet.fromEncodedString(argStr);
396        }
397
398        final String reason = "Could not parse value '" + argValueRepr.asString() + "' as a " + objectSpec.getFullIdentifier();
399        argRepr.mapPut("invalidReason", reason);
400        throw new IllegalArgumentException(reason);
401    }
402
403    public static void appendValueAndFormat(ObjectSpecification objectSpec, ObjectAdapter objectAdapter, JsonRepresentation repr) {
404
405        final JsonValueConverter jvc = converterBySpec.get(objectSpec.getSpecId());
406        if(jvc != null) {
407            jvc.appendValueAndFormat(objectAdapter, repr);
408        } else {
409            final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
410            if (encodableFacet == null) {
411                throw new IllegalArgumentException("objectSpec expected to have EncodableFacet");
412            }
413            Object value = objectAdapter != null? encodableFacet.toEncodedString(objectAdapter): NullNode.getInstance();
414            append(repr, value, "decimal", "bigdecimal");
415        }
416    }
417    
418    public static Object asObject(final ObjectAdapter objectAdapter) {
419        if (objectAdapter == null) {
420            throw new IllegalArgumentException("objectAdapter cannot be null");
421        }
422        final ObjectSpecification objectSpec = objectAdapter.getSpecification();
423
424        final JsonValueConverter jvc = converterBySpec.get(objectSpec.getSpecId());
425        if(jvc != null) {
426            return jvc.asObject(objectAdapter);
427        } 
428        
429        // else
430        final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
431        if (encodableFacet == null) {
432            throw new IllegalArgumentException("objectSpec expected to have EncodableFacet");
433        }
434        return encodableFacet.toEncodedString(objectAdapter);
435    }
436
437    
438
439    private static void append(JsonRepresentation repr, Object value, String format, String xIsisFormat) {
440        repr.mapPut("value", value);
441        if(format != null) {
442            repr.mapPut("format", format);
443        }
444        if(xIsisFormat != null) {
445            repr.mapPut("x-isis-format", xIsisFormat);
446        }
447    }
448
449    private static void append(JsonRepresentation repr, ObjectAdapter value, String format, String xIsisFormat) {
450        append(repr, unwrap(value), format, xIsisFormat);
451    }
452    
453    private static Object unwrap(ObjectAdapter objectAdapter) {
454        return objectAdapter != null? objectAdapter.getObject(): NullNode.getInstance();
455    }
456
457
458
459    private static ObjectAdapter adapterFor(Object value) {
460        return getAdapterManager().adapterFor(value);
461    }
462    
463    public static AdapterManager getAdapterManager() {
464        return IsisContext.getPersistenceSession().getAdapterManager();
465    }
466
467}