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.List;
025import java.util.Map;
026import com.google.common.base.Function;
027import com.google.common.collect.Iterables;
028import com.google.common.collect.Lists;
029import com.google.common.collect.Maps;
030import org.codehaus.jackson.node.NullNode;
031import org.joda.time.DateTime;
032import org.joda.time.LocalDate;
033import org.joda.time.LocalDateTime;
034import org.joda.time.format.DateTimeFormat;
035import org.joda.time.format.DateTimeFormatter;
036import org.joda.time.format.ISODateTimeFormat;
037import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
038import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager;
039import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet;
040import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
041import org.apache.isis.core.metamodel.spec.ObjectSpecId;
042import org.apache.isis.core.metamodel.spec.ObjectSpecification;
043import org.apache.isis.core.runtime.system.context.IsisContext;
044import org.apache.isis.viewer.restfulobjects.applib.JsonRepresentation;
045
046/**
047 * Similar to Isis' value encoding, but with additional support for JSON
048 * primitives.
049 */
050public final class JsonValueEncoder {
051
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, String format);
084        
085        public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
086            repr.mapPut("value", unwrapAsObjectElseNullNode(objectAdapter));
087            appendFormats(repr, this.format, this.xIsisFormat);
088        }
089
090        public Object asObject(ObjectAdapter objectAdapter, String format) {
091            return objectAdapter.getObject();
092        }
093    }
094    
095    private static Map<ObjectSpecId, JsonValueConverter> converterBySpec = Maps.newLinkedHashMap();
096    
097    private static void putConverter(JsonValueConverter jvc) {
098        final List<ObjectSpecId> specIds = jvc.getSpecIds();
099        for (ObjectSpecId specId : specIds) {
100            converterBySpec.put(specId, jvc);
101        }
102    }
103
104    static {
105        putConverter(new JsonValueConverter(null, "string", String.class){
106            @Override
107            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
108                if (repr.isString()) {
109                    return adapterFor(repr.asString());
110                } 
111                return null;
112            }
113            @Override
114            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
115                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
116                if(obj instanceof String) {
117                    final String str = (String) obj;
118                    repr.mapPut("value", str);
119                } else {
120                    repr.mapPut("value", obj);
121                }
122                appendFormats(repr, this.format, xIsisFormat);
123            }
124        });
125
126        putConverter(new JsonValueConverter(null, "boolean", boolean.class, Boolean.class){
127            @Override
128            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
129                if (repr.isBoolean()) {
130                    return adapterFor(repr.asBoolean());
131                } 
132                return null;
133            }
134            @Override
135            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
136                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
137                if(obj instanceof Boolean) {
138                    final Boolean b = (Boolean) obj;
139                    repr.mapPut("value", b);
140                } else {
141                    repr.mapPut("value", obj);
142                }
143                appendFormats(repr, this.format, xIsisFormat);
144            }
145        });
146        
147        putConverter(new JsonValueConverter("int", "byte", byte.class, Byte.class){
148            @Override
149            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
150                if (repr.isNumber()) {
151                    return adapterFor(repr.asNumber().byteValue());
152                }
153                if (repr.isInt()) {
154                    return adapterFor((byte)(int)repr.asInt());
155                }
156                if (repr.isLong()) {
157                    return adapterFor((byte)(long)repr.asLong());
158                }
159                if (repr.isBigInteger()) {
160                    return adapterFor(repr.asBigInteger().byteValue());
161                }
162                return null;
163            }
164            @Override
165            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
166                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
167                if(obj instanceof Byte) {
168                    final Byte b = (Byte) obj;
169                    repr.mapPut("value", b);
170                } else {
171                    repr.mapPut("value", obj);
172                }
173                appendFormats(repr, this.format, xIsisFormat);
174            }
175        });
176        
177        putConverter(new JsonValueConverter("int", "short", short.class, Short.class){
178            @Override
179            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
180                if (repr.isNumber()) {
181                    return adapterFor(repr.asNumber().shortValue());
182                }
183                if (repr.isInt()) {
184                    return adapterFor((short)(int)repr.asInt());
185                }
186                if (repr.isLong()) {
187                    return adapterFor((short)(long)repr.asLong());
188                }
189                if (repr.isBigInteger()) {
190                    return adapterFor(repr.asBigInteger().shortValue());
191                }
192                return null;
193            }
194            @Override
195            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
196                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
197                if(obj instanceof Short) {
198                    final Short s = (Short) obj;
199                    repr.mapPut("value", s);
200                } else {
201                    repr.mapPut("value", obj);
202                }
203                appendFormats(repr, this.format, xIsisFormat);
204            }
205        });
206        
207        putConverter(new JsonValueConverter("int", "int", int.class, Integer.class){
208            @Override
209            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
210                if (repr.isInt()) {
211                    return adapterFor(repr.asInt());
212                }
213                if (repr.isLong()) {
214                    return adapterFor((int)(long)repr.asLong());
215                }
216                if (repr.isBigInteger()) {
217                    return adapterFor(repr.asBigInteger().intValue());
218                }
219                if (repr.isNumber()) {
220                    return adapterFor(repr.asNumber().intValue());
221                }
222                return null;
223            }
224            @Override
225            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
226                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
227                if(obj instanceof Integer) {
228                    final Integer i = (Integer) obj;
229                    repr.mapPut("value", i);
230                } else {
231                    repr.mapPut("value", obj);
232                }
233                appendFormats(repr, this.format, xIsisFormat);
234            }
235        });
236        
237        putConverter(new JsonValueConverter("int", "long", long.class, Long.class){
238            @Override
239            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
240                if (repr.isLong()) {
241                    return adapterFor(repr.asLong());
242                }
243                if (repr.isInt()) {
244                    return adapterFor(repr.asInt());
245                }
246                if (repr.isBigInteger()) {
247                    return adapterFor(repr.asBigInteger().longValue());
248                }
249                if (repr.isNumber()) {
250                    return adapterFor(repr.asNumber().longValue());
251                }
252                return null;
253            }
254            @Override
255            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
256                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
257                if(obj instanceof Long) {
258                    final Long l = (Long) obj;
259                    repr.mapPut("value", l);
260                } else {
261                    repr.mapPut("value", obj);
262                }
263                appendFormats(repr, this.format, xIsisFormat);
264            }
265        });
266        
267        putConverter(new JsonValueConverter("decimal", "float", float.class, Float.class){
268            @Override
269            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
270                if (repr.isDouble()) {
271                    return adapterFor((float)(double)repr.asDouble());
272                }
273                if (repr.isNumber()) {
274                    return adapterFor(repr.asNumber().floatValue());
275                }
276                if (repr.isLong()) {
277                    return adapterFor((float)repr.asLong());
278                }
279                if (repr.isInt()) {
280                    return adapterFor((float)repr.asInt());
281                }
282                if (repr.isBigInteger()) {
283                    return adapterFor(repr.asBigInteger().floatValue());
284                }
285                return null;
286            }
287            @Override
288            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
289                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
290                if(obj instanceof Float) {
291                    final Float f = (Float) obj;
292                    repr.mapPut("value", f);
293                } else {
294                    repr.mapPut("value", obj);
295                }
296                appendFormats(repr, this.format, xIsisFormat);
297            }
298        });
299        
300        putConverter(new JsonValueConverter("decimal", "double", double.class, Double.class){
301            @Override
302            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
303                if (repr.isDouble()) {
304                    return adapterFor(repr.asDouble());
305                }
306                if (repr.isLong()) {
307                    return adapterFor((double)repr.asLong());
308                }
309                if (repr.isInt()) {
310                    return adapterFor((double)repr.asInt());
311                }
312                if (repr.isBigInteger()) {
313                    return adapterFor(repr.asBigInteger().doubleValue());
314                }
315                if (repr.isBigDecimal()) {
316                    return adapterFor(repr.asBigDecimal().doubleValue());
317                }
318                if (repr.isNumber()) {
319                    return adapterFor(repr.asNumber().doubleValue());
320                }
321                return null;
322            }
323            @Override
324            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
325                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
326                if(obj instanceof Double) {
327                    final Double d = (Double) obj;
328                    repr.mapPut("value", d);
329                } else {
330                    repr.mapPut("value", obj);
331                }
332                appendFormats(repr, this.format, xIsisFormat);
333            }
334        });
335        
336        putConverter(new JsonValueConverter(null, "char", char.class, Character.class){
337            @Override
338            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
339                if (repr.isString()) {
340                    final String str = repr.asString();
341                    if(str != null && str.length()>0) {
342                        return adapterFor(str.charAt(0));
343                    }
344                }
345                // in case a char literal was provided
346                if(repr.isInt()) {
347                    final Integer x = repr.asInt();
348                    if(Character.MIN_VALUE <= x && x <= Character.MAX_VALUE) {
349                        char c = (char) x.intValue();
350                        return adapterFor(c);
351                    }
352                }
353                return null;
354            }
355            @Override
356            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
357                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
358                if(obj instanceof Character) {
359                    final Character c = (Character) obj;
360                    repr.mapPut("value", c);
361                } else {
362                    repr.mapPut("value", obj);
363                }
364                appendFormats(repr, this.format, xIsisFormat);
365            }
366        });
367        
368        putConverter(new JsonValueConverter("big-integer(18)", "javamathbiginteger", BigInteger.class){
369            @Override
370            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
371                if (repr.isString()) {
372                    return adapterFor(new BigInteger(repr.asString()));
373                }
374                if (repr.isBigInteger()) {
375                    return adapterFor(repr.asBigInteger(format));
376                }
377                if (repr.isLong()) {
378                    return adapterFor(BigInteger.valueOf(repr.asLong()));
379                }
380                if (repr.isInt()) {
381                    return adapterFor(BigInteger.valueOf(repr.asInt()));
382                }
383                if (repr.isNumber()) {
384                    return adapterFor(BigInteger.valueOf(repr.asNumber().longValue()));
385                }
386                return null;
387            }
388            @Override
389            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
390                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
391                if(obj instanceof BigInteger) {
392                    final BigInteger bi = (BigInteger) obj;
393                    repr.mapPut("value", bi);
394                } else {
395                    repr.mapPut("value", obj);
396                }
397                appendFormats(repr, format != null? format: this.format, xIsisFormat);
398            }
399        });
400        
401        putConverter(new JsonValueConverter("big-decimal", "javamathbigdecimal", BigDecimal.class){
402            @Override
403            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
404                if (repr.isString()) {
405                    return adapterFor(new BigDecimal(repr.asString()));
406                }
407                if (repr.isBigDecimal()) {
408                    return adapterFor(repr.asBigDecimal(format));
409                }
410                if (repr.isBigInteger()) {
411                    return adapterFor(new BigDecimal(repr.asBigInteger()));
412                }
413                if (repr.isDouble()) {
414                    return adapterFor(BigDecimal.valueOf(repr.asDouble()));
415                }
416                if (repr.isLong()) {
417                    return adapterFor(BigDecimal.valueOf(repr.asLong()));
418                }
419                if (repr.isInt()) {
420                    return adapterFor(BigDecimal.valueOf(repr.asInt()));
421                }
422                return null;
423            }
424            @Override
425            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
426                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
427                if(obj instanceof BigDecimal) {
428                    final BigDecimal bd = (BigDecimal) obj;
429                    repr.mapPut("value", bd);
430                } else {
431                    repr.mapPut("value", obj);
432                }
433                appendFormats(repr, format != null ? format : this.format, xIsisFormat);
434            }
435        });
436
437        putConverter(new JsonValueConverter("date", "jodalocaldate", LocalDate.class){
438
439            // these formatters do NOT use withZoneUTC()
440            final List<DateTimeFormatter> formatters = Arrays.asList(
441                    ISODateTimeFormat.date(),
442                    ISODateTimeFormat.basicDate(),
443                    DateTimeFormat.forPattern("yyyyMMdd"),
444                    JsonRepresentation.yyyyMMdd
445                    );
446
447            @Override
448            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
449                if (repr.isString()) {
450                    final String dateStr = repr.asString();
451                    for (DateTimeFormatter formatter : formatters) {
452                        try {
453                            final LocalDate parsedDate = formatter.parseLocalDate(dateStr);
454                            return adapterFor(parsedDate);
455                        } catch (IllegalArgumentException ex) {
456                            // fall through
457                        }
458                    }
459                }
460                return null;
461            }
462
463            @Override
464            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
465                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
466                if(obj instanceof LocalDate) {
467                    final LocalDate date = (LocalDate) obj;
468                    final String dateStr = formatters.get(0).print(date.toDateTimeAtStartOfDay());
469                    repr.mapPut("value", dateStr);
470                } else {
471                    repr.mapPut("value", obj);
472                }
473                appendFormats(repr, this.format, xIsisFormat);
474            }
475        });
476
477        putConverter(new JsonValueConverter("date-time", "jodalocaldatetime", LocalDateTime.class){
478
479            final List<DateTimeFormatter> formatters = Arrays.asList(
480                    ISODateTimeFormat.dateTimeNoMillis().withZoneUTC(),
481                    ISODateTimeFormat.dateTime().withZoneUTC(),
482                    ISODateTimeFormat.basicDateTimeNoMillis().withZoneUTC(),
483                    ISODateTimeFormat.basicDateTime().withZoneUTC(),
484                    JsonRepresentation.yyyyMMddTHHmmssZ.withZoneUTC()
485                    );
486            
487            @Override
488            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
489                if (repr.isString()) {
490                    final String dateStr = repr.asString();
491                    for (DateTimeFormatter formatter : formatters) {
492                        try {
493                            final LocalDateTime parsedDate = formatter.parseLocalDateTime(dateStr);
494                            return adapterFor(parsedDate);
495                        } catch (IllegalArgumentException ex) {
496                            // fall through
497                        }
498                    }
499                }
500                return null;
501            }
502
503            @Override
504            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
505                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
506                if(obj instanceof LocalDateTime) {
507                    final LocalDateTime date = (LocalDateTime) obj;
508                    final String dateStr = formatters.get(0).print(date.toDateTime());
509                    repr.mapPut("value", dateStr);
510                } else {
511                    repr.mapPut("value", obj);
512                }
513                appendFormats(repr, this.format, xIsisFormat);
514            }
515        });
516
517        putConverter(new JsonValueConverter("date-time", "jodadatetime", DateTime.class){
518            
519            final List<DateTimeFormatter> formatters = Arrays.asList(
520                    ISODateTimeFormat.dateTimeNoMillis().withZoneUTC(),
521                    ISODateTimeFormat.dateTime().withZoneUTC(),
522                    ISODateTimeFormat.basicDateTimeNoMillis().withZoneUTC(),
523                    ISODateTimeFormat.basicDateTime().withZoneUTC(),
524                    JsonRepresentation.yyyyMMddTHHmmssZ.withZoneUTC()
525            );
526            
527            @Override
528            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
529                if (repr.isString()) {
530                    final String dateStr = repr.asString();
531                    for (DateTimeFormatter formatter : formatters) {
532                        try {
533                            final DateTime parsedDate = formatter.parseDateTime(dateStr);
534                            return adapterFor(parsedDate);
535                        } catch (IllegalArgumentException ex) {
536                            // fall through
537                        }
538                    }
539                }
540                return null;
541            }
542
543            @Override
544            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
545                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
546                if(obj instanceof DateTime) {
547                    final DateTime date = (DateTime) obj;
548                    final String dateStr = formatters.get(0).print(date.toDateTime());
549                    repr.mapPut("value", dateStr);
550                } else {
551                    repr.mapPut("value", obj);
552                }
553                appendFormats(repr, this.format, xIsisFormat);
554            }
555        });
556
557        putConverter(new JsonValueConverter("date-time", "javautildate", java.util.Date.class){
558            
559            final List<DateTimeFormatter> formatters = Arrays.asList(
560                    ISODateTimeFormat.dateTimeNoMillis().withZoneUTC(),
561                    ISODateTimeFormat.dateTime().withZoneUTC(),
562                    ISODateTimeFormat.basicDateTimeNoMillis().withZoneUTC(),
563                    ISODateTimeFormat.basicDateTime().withZoneUTC(),
564                    JsonRepresentation.yyyyMMddTHHmmssZ.withZoneUTC()
565                    );
566            @Override
567            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
568                if (repr.isString()) {
569                    final String dateStr = repr.asString();
570                    for (DateTimeFormatter formatter : formatters) {
571                        try {
572                            final DateTime parseDateTime = formatter.parseDateTime(dateStr);
573                            final java.util.Date parsedDate = parseDateTime.toDate();
574                            return adapterFor(parsedDate);
575                        } catch (IllegalArgumentException ex) {
576                            // fall through
577                        }
578                    }
579                }
580                return null;
581            }
582
583            @Override
584            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
585                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
586                if(obj instanceof java.util.Date) {
587                    final java.util.Date date = (java.util.Date) obj;
588                    final DateTimeFormatter dateTimeFormatter = formatters.get(0);
589                    final String dateStr = dateTimeFormatter.print(new DateTime(date));
590                    repr.mapPut("value", dateStr);
591                } else {
592                    repr.mapPut("value", obj);
593                }
594                appendFormats(repr, this.format, xIsisFormat);
595            }
596        });
597
598        putConverter(new JsonValueConverter("date", "javasqldate", java.sql.Date.class){
599            
600            final List<DateTimeFormatter> formatters = Arrays.asList(
601                    ISODateTimeFormat.date().withZoneUTC(),
602                    ISODateTimeFormat.basicDate().withZoneUTC(),
603                    JsonRepresentation.yyyyMMdd.withZoneUTC()
604                    );
605            @Override
606            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
607                if (repr.isString()) {
608                    final String dateStr = repr.asString();
609                    for (DateTimeFormatter formatter : formatters) {
610                        try {
611                            final DateTime parseDateTime = formatter.parseDateTime(dateStr);
612                            final java.sql.Date parsedDate = new java.sql.Date(parseDateTime.getMillis());
613                            return adapterFor(parsedDate);
614                        } catch (IllegalArgumentException ex) {
615                            // fall through
616                        }
617                    }
618                }
619                return null;
620            }
621
622            @Override
623            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
624                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
625                if(obj instanceof java.sql.Date) {
626                    final java.sql.Date date = (java.sql.Date) obj;
627                    final String dateStr = formatters.get(0).print(new DateTime(date));
628                    repr.mapPut("value", dateStr);
629                } else {
630                    repr.mapPut("value", obj);
631                }
632                appendFormats(repr, this.format, xIsisFormat);
633            }
634        });
635
636        putConverter(new JsonValueConverter("time", "javasqltime", java.sql.Time.class){
637            
638            final List<DateTimeFormatter> formatters = Arrays.asList(
639                        ISODateTimeFormat.hourMinuteSecond().withZoneUTC(),
640                        ISODateTimeFormat.basicTimeNoMillis().withZoneUTC(),
641                        ISODateTimeFormat.basicTime().withZoneUTC(),
642                        JsonRepresentation._HHmmss.withZoneUTC()
643            );
644            @Override
645            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
646                if (repr.isString()) {
647                    final String dateStr = repr.asString();
648                    for (DateTimeFormatter formatter : formatters) {
649                        try {
650                            final DateTime parseDateTime = formatter.parseDateTime(dateStr);
651                            final java.sql.Time parsedTime = new java.sql.Time(parseDateTime.getMillis());
652                            return adapterFor(parsedTime);
653                        } catch (IllegalArgumentException ex) {
654                            // fall through
655                        }
656                    }
657                }
658                return null;
659            }
660
661            @Override
662            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
663                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
664                if(obj instanceof java.sql.Time) {
665                    final java.sql.Time date = (java.sql.Time) obj;
666                    final String dateStr = formatters.get(0).print(new DateTime(date));
667                    repr.mapPut("value", dateStr);
668                } else {
669                    repr.mapPut("value", obj);
670                }
671                appendFormats(repr, this.format, xIsisFormat);
672            }
673        });
674
675        putConverter(new JsonValueConverter("utc-millisec", "javasqltimestamp", java.sql.Timestamp.class){
676            
677            @Override
678            public ObjectAdapter asAdapter(JsonRepresentation repr, String format) {
679                if (repr.isLong()) {
680                    final Long millis = repr.asLong();
681                    final java.sql.Timestamp parsedTimestamp = new java.sql.Timestamp(millis);
682                    return adapterFor(parsedTimestamp);
683                }
684                if (repr.isString()) {
685                    final String dateStr = repr.asString();
686                    try {
687                        final Long parseMillis = Long.parseLong(dateStr);
688                        final java.sql.Timestamp parsedTimestamp = new java.sql.Timestamp(parseMillis);
689                        return adapterFor(parsedTimestamp);
690                    } catch (IllegalArgumentException ex) {
691                        // fall through
692                    }
693                }
694                return null;
695            }
696
697            @Override
698            public void appendValueAndFormat(ObjectAdapter objectAdapter, String format, JsonRepresentation repr) {
699                final Object obj = unwrapAsObjectElseNullNode(objectAdapter);
700                if(obj instanceof java.sql.Timestamp) {
701                    final java.sql.Timestamp date = (java.sql.Timestamp) obj;
702                    final long millisStr = date.getTime();
703                    repr.mapPut("value", millisStr);
704                } else {
705                    repr.mapPut("value", obj);
706                }
707                appendFormats(repr, this.format, xIsisFormat);
708            }
709        });
710    }
711
712
713
714    public static ObjectAdapter asAdapter(final ObjectSpecification objectSpec, final JsonRepresentation argValueRepr, final String format) {
715        if(argValueRepr == null) {
716            return null;
717        }
718        if (objectSpec == null) {
719            throw new IllegalArgumentException("ObjectSpecification is required");
720        }
721        if (!argValueRepr.isValue()) {
722            throw new IllegalArgumentException("Representation must be of a value");
723        }
724        final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
725        if (encodableFacet == null) {
726            String reason = "ObjectSpec expected to have an EncodableFacet";
727            throw new IllegalArgumentException(reason);
728        }
729
730        final ObjectSpecId specId = objectSpec.getSpecId();
731        final JsonValueConverter jvc = converterBySpec.get(specId);
732        if(jvc == null) {
733            // best effort
734            if (argValueRepr.isString()) {
735                final String argStr = argValueRepr.asString();
736                return encodableFacet.fromEncodedString(argStr);
737            }
738
739            throw new IllegalArgumentException("Unable to parse value");
740        }
741
742        final ObjectAdapter asAdapter = jvc.asAdapter(argValueRepr, format);
743        if(asAdapter != null) {
744            return asAdapter;
745        }
746        
747        // last attempt
748        if (argValueRepr.isString()) {
749            final String argStr = argValueRepr.asString();
750            try {
751                return encodableFacet.fromEncodedString(argStr);
752            } catch(TextEntryParseException ex) {
753                throw new IllegalArgumentException(ex.getMessage());
754            }
755        }
756
757        throw new IllegalArgumentException("Could not parse value '" + argValueRepr.asString() + "' as a " + objectSpec.getFullIdentifier());
758    }
759
760    public static void appendValueAndFormat(ObjectSpecification objectSpec, ObjectAdapter objectAdapter, JsonRepresentation repr, String format) {
761
762        final JsonValueConverter jvc = converterBySpec.get(objectSpec.getSpecId());
763        if(jvc != null) {
764            jvc.appendValueAndFormat(objectAdapter, format, repr);
765        } else {
766            final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
767            if (encodableFacet == null) {
768                throw new IllegalArgumentException("objectSpec expected to have EncodableFacet");
769            }
770            Object value = objectAdapter != null? encodableFacet.toEncodedString(objectAdapter): NullNode.getInstance();
771            repr.mapPut("value", value);
772            appendFormats(repr, "string", "string");
773        }
774    }
775    
776    public static Object asObject(final ObjectAdapter objectAdapter, final String format) {
777        if (objectAdapter == null) {
778            throw new IllegalArgumentException("objectAdapter cannot be null");
779        }
780        final ObjectSpecification objectSpec = objectAdapter.getSpecification();
781
782        final JsonValueConverter jvc = converterBySpec.get(objectSpec.getSpecId());
783        if(jvc != null) {
784            return jvc.asObject(objectAdapter, format);
785        } 
786        
787        // else
788        final EncodableFacet encodableFacet = objectSpec.getFacet(EncodableFacet.class);
789        if (encodableFacet == null) {
790            throw new IllegalArgumentException("objectSpec expected to have EncodableFacet");
791        }
792        return encodableFacet.toEncodedString(objectAdapter);
793    }
794
795
796    private static void appendFormats(JsonRepresentation repr, String format, String xIsisFormat) {
797        if(format != null) {
798            repr.mapPut("format", format);
799        }
800        if(xIsisFormat != null) {
801            repr.mapPut("extensions.x-isis-format", xIsisFormat);
802        }
803    }
804
805    private static Object unwrapAsObjectElseNullNode(ObjectAdapter objectAdapter) {
806        return objectAdapter != null? objectAdapter.getObject(): NullNode.getInstance();
807    }
808
809
810
811    private static ObjectAdapter adapterFor(Object value) {
812        return getAdapterManager().adapterFor(value);
813    }
814
815    private static AdapterManager testAdapterManager;
816
817    // for testing purposes only
818    static void testSetAdapterManager(AdapterManager adapterManager) {
819        JsonValueEncoder.testAdapterManager = adapterManager;
820    }
821
822    public static AdapterManager getAdapterManager() {
823        return testAdapterManager != null? testAdapterManager:  IsisContext.getPersistenceSession().getAdapterManager();
824    }
825
826}