(JSON Parser State) Reduce Cyclomatic Complexity of ParseJson
[platform/core/uifw/dali-toolkit.git] / dali-toolkit / internal / builder / json-parser-state.cpp
1 /*
2  * Copyright (c) 2014 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17
18 // CLASS HEADER
19 #include <dali-toolkit/internal/builder/json-parser-state.h>
20
21 // EXTERNAL INCLUDES
22 #include <string>
23 #include <algorithm>
24
25 namespace Dali
26 {
27
28 namespace Toolkit
29 {
30
31 namespace Internal
32 {
33
34 namespace
35 {
36
37 // true if character represent a digit
38 inline bool IsDigit(char c)
39 {
40   return (c >= '0' && c <= '9');
41 }
42
43 // convert string to integer
44 bool StringToInteger(const char *first, const char *last, int& out)
45 {
46   int sign = 1;
47   if (first != last)
48   {
49     if (*first == '-')
50     {
51       sign = -1;
52       ++first;
53     }
54     else if (*first == '+')
55     {
56       ++first;
57     }
58   }
59
60   // json error for int starting with zero
61   if( 0 == (*first - '0') && (first+1 != last))
62   {
63     return false;
64   }
65
66   int result = 0;
67   for (; first != last && IsDigit(*first); ++first)
68   {
69     result = 10 * result + (*first - '0');
70   }
71   out = result * sign;
72
73   if(first != last)
74   {
75     return false;
76   }
77   else
78   {
79     return true;
80   }
81 }
82
83 // convert hexadecimal string to unsigned integer
84 bool HexStringToUnsignedInteger(const char *first, const char *last, unsigned int& out)
85 {
86   unsigned int result = 0;
87   for (; first != last; ++first)
88   {
89     int digit;
90     if (IsDigit(*first))
91     {
92       digit = *first - '0';
93     }
94     else if (*first >= 'a' && *first <= 'f')
95     {
96       digit = *first - 'a' + 10;
97     }
98     else if (*first >= 'A' && *first <= 'F')
99     {
100       digit = *first - 'A' + 10;
101     }
102     else
103     {
104       break;
105     }
106     result = 16 * result + digit;
107   }
108   out = result;
109
110   if(first != last)
111   {
112     return false;
113   }
114   else
115   {
116     return true;
117   }
118 }
119
120 // convert string to floating point
121 bool StringToFloat(const char* first, const char* last, float& out)
122 {
123   // sign
124   float sign = 1;
125   if (first != last)
126   {
127     if (*first == '-')
128     {
129       sign = -1;
130       ++first;
131     }
132     else if (*first == '+')
133     {
134       ++first;
135     }
136   }
137
138   // integer part
139   float result = 0;
140   for (; first != last && IsDigit(*first); ++first)
141   {
142     result = 10 * result + (*first - '0');
143   }
144
145   // fraction part
146   if (first != last && *first == '.')
147   {
148     ++first;
149
150     float inv_base = 0.1f;
151     for (; first != last && IsDigit(*first); ++first)
152     {
153       result += (*first - '0') * inv_base;
154       inv_base *= 0.1f;
155     }
156   }
157
158   // result w\o exponent
159   result *= sign;
160
161   // exponent
162   bool exponent_negative = false;
163   int exponent = 0;
164   if (first != last && (*first == 'e' || *first == 'E'))
165   {
166     ++first;
167
168     if (*first == '-')
169     {
170       exponent_negative = true;
171       ++first;
172     }
173     else if (*first == '+')
174     {
175       ++first;
176     }
177
178     if(first == last || !IsDigit(*first))
179     {
180       return false;
181     }
182
183     for (; first != last && IsDigit(*first); ++first)
184     {
185       exponent = 10 * exponent + (*first - '0');
186     }
187   }
188
189   if (exponent)
190   {
191     float power_of_ten = 10;
192     for (; exponent > 1; exponent--)
193     {
194       power_of_ten *= 10;
195     }
196
197     if (exponent_negative)
198     {
199       result /= power_of_ten;
200     }
201     else
202     {
203       result *= power_of_ten;
204     }
205   }
206
207   out = result;
208
209   if(first != last)
210   {
211     return false;
212   }
213   else
214   {
215     return true;
216   }
217 }
218
219
220 bool IsNumber(char c)
221 {
222   bool ret = false;
223   switch( c )
224   {
225     case '0':
226     case '1':
227     case '2':
228     case '3':
229     case '4':
230     case '5':
231     case '6':
232     case '7':
233     case '8':
234     case '9':
235     {
236       ret = true;
237       break;
238     }
239     default:
240     {
241       ret = false;
242     }
243   }
244   return ret;
245 }
246
247 } // anon namespace
248
249
250 JsonParserState::JsonParserState(TreeNode* _root)
251   : mRoot(_root), mCurrent(_root),
252     mErrorDescription(nullptr), mErrorNewLine(0), mErrorColumn(0), mErrorPosition(0),
253     mNumberOfParsedChars(0), mNumberOfCreatedNodes(0), mFirstParse(false),
254     mState(STATE_START)
255 {
256   if(_root == nullptr)
257   {
258     mFirstParse = true;
259   }
260 }
261
262 TreeNode* JsonParserState::CreateNewNode(const char* name, TreeNode::NodeType type)
263 {
264   TreeNode* node = nullptr;
265
266   node = TreeNodeManipulator::NewTreeNode();
267   TreeNodeManipulator modifyNew(node);
268   modifyNew.SetType(type);
269   modifyNew.SetName(name);
270   if(mRoot == nullptr)
271   {
272     mRoot    = node;
273     mCurrent = TreeNodeManipulator(mRoot);
274   }
275   else
276   {
277     mCurrent.AddChild(node);
278     mCurrent = modifyNew;
279   }
280
281   ++mNumberOfCreatedNodes;
282
283   return node;
284
285 }
286
287 TreeNode* JsonParserState::NewNode(const char* name, TreeNode::NodeType type)
288 {
289   TreeNode* node = nullptr;
290
291   if(mFirstParse)
292   {
293     node = CreateNewNode(name, type);
294   }
295   else
296   {
297     // a merging parse
298
299     if(name)
300     {
301       const TreeNode* found = mCurrent.GetChild(name);
302       if( nullptr != found )
303       {
304         node = const_cast<TreeNode*>(found);
305       }
306     }
307     else
308     {
309       // if root node
310       if( mCurrent.GetParent() == nullptr )
311       {
312         node = mRoot;
313       }
314     }
315
316     if(node)
317     {
318       // walk tree and deallocate children as were replacing this node
319       TreeNodeManipulator modify(node);
320
321       modify.SetName(name);
322
323       // Set the type of the existing node.
324       // Where the new type is different, then any children of this node will
325       // be deleted.
326       // When the type is an array of numbers, then this will also remove any children
327       // When the type is an object or other array, then the children will not be removed,
328       // but will instead follow these replace rules.
329       modify.SetType(type);
330
331       mCurrent = modify;
332     }
333     else
334     {
335       // if not found then create new
336       node = CreateNewNode(name, type);
337     }
338   }
339
340   return node;
341 }
342
343 TreeNode* JsonParserState::GetRoot()
344 {
345   return mRoot;
346 }
347
348 bool JsonParserState::Error(const char* description)
349 {
350   mErrorDescription = description;
351   return false;
352 }
353
354 bool JsonParserState::ParseWhiteSpace()
355 {
356   bool c_comment   = false;
357   bool cpp_comment = false;
358
359   if( mIter == mEnd )
360   {
361     return true;
362   }
363
364   // skip white space
365   char nextChar = 0;
366   while(1)
367   {
368     char c = Char();
369
370     if(c == '\xA')
371     {
372       NewLine();
373     }
374
375     if( AtLeast(2) )
376     {
377       nextChar = mIter[1];
378     }
379     else
380     {
381       nextChar = 0;
382     }
383
384     if( cpp_comment )
385     {
386       if( '\n' == c )
387       {
388         cpp_comment = false;
389         Advance(1);
390         continue; // rather than carry on as comments may be back to back
391       }
392     }
393     else if( !c_comment && (c == '/' && nextChar == '/') )
394     {
395       cpp_comment = true;
396     }
397
398     if( c_comment )
399     {
400       if( c == '*' && nextChar == '/' )
401       {
402         c_comment = false;
403         Advance(2);
404         continue;
405       }
406     }
407     else if( !cpp_comment && (c == '/' && nextChar == '*') )
408     {
409       c_comment = true;
410     }
411
412     if( ! (c_comment || cpp_comment) )
413     {
414       if( ! (c == '\x20' || c == '\x9' || c == '\xD' || c == '\xA' ) )
415       {
416         break;
417       }
418     }
419
420     if( AdvanceEnded(1) )
421     {
422       break;
423     }
424
425   } // while(1)
426
427   return true;
428 } // ParseWhiteSpace
429
430 bool JsonParserState::ParseSymbol(const std::string& symbol)
431 {
432   if( AtLeast( symbol.size() ) )
433   {
434     for(int i = 0; i < static_cast<int>( symbol.size() ); ++i)
435     {
436       if(*mIter != symbol[i])
437       {
438         return false;
439       }
440       Advance(1);
441     }
442     return true;
443   }
444   else
445   {
446     return false;
447   }
448 }
449
450 bool JsonParserState::ParseTrue()
451 {
452   if( ParseSymbol("true") )
453   {
454     mCurrent.SetInteger(1);
455     mCurrent.SetType(TreeNode::BOOLEAN);
456     return true;
457   }
458   else
459   {
460     return Error("Unexpected character; expected symbol ie 'true'");
461   }
462 }
463
464 bool JsonParserState::ParseFalse()
465 {
466   if( ParseSymbol("false") )
467   {
468     mCurrent.SetInteger(0);
469     mCurrent.SetType(TreeNode::BOOLEAN);
470     return true;
471   }
472   else
473   {
474     return Error("Unexpected character; expected symbol ie 'false'");
475   }
476 }
477
478 bool JsonParserState::ParseNULL()
479 {
480   if( ParseSymbol("null") )
481   {
482     mCurrent.SetType(TreeNode::IS_NULL);
483     return true;
484   }
485   else
486   {
487     return Error("Unexpected character; expected symbol ie 'null'");
488   }
489 }
490
491 bool JsonParserState::ParseNumber()
492 {
493   mCurrent.SetType( TreeNode::INTEGER );
494
495   VectorCharIter first = mIter;
496   char c = Char();
497
498   if( !(c == '-' || IsNumber(c) ) )
499   {
500     return Error("Number must start with '-' or 0-9");
501   }
502
503   while ( IsNumber(c) || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-' )
504   {
505     if (c == '.' || c == 'e' || c == 'E')
506     {
507       mCurrent.SetType( TreeNode::FLOAT );
508     }
509     Advance(1);
510     c = Char();
511   }
512
513   if( mCurrent.GetType() == TreeNode::INTEGER )
514   {
515     int i = 0;
516     if( StringToInteger(&(*first), &(*mIter), i ) )
517     {
518       mCurrent.SetInteger(i);
519     }
520     else
521     {
522       return Error("Bad integer number");
523     }
524   }
525
526   if(mCurrent.GetType() == TreeNode::FLOAT)
527   {
528     float f = 0.f;
529     if( StringToFloat(&(*first), &(*mIter), f) )
530     {
531       mCurrent.SetFloat(f);
532     }
533     else
534     {
535       return Error("Bad float number");
536     }
537   }
538
539   return (mCurrent.GetType() == TreeNode::INTEGER)  || (mCurrent.GetType() == TreeNode::FLOAT);
540 }
541
542 char* JsonParserState::EncodeString()
543 {
544   int substitution = 0;
545   VectorCharIter first = mIter;
546   VectorCharIter last  = mIter;
547
548   while (*mIter)
549   {
550     if (static_cast<unsigned char>(*mIter) < '\x20')
551     {
552       static_cast<void>( Error("Control characters not allowed in strings") );
553       return nullptr;
554     }
555     else if (*mIter == '\\' && AtLeast(2))
556     {
557       switch (*(mIter+1))
558       {
559         case '"':
560         {
561           *last = '"';
562           break;
563         }
564         case '\\':
565         {
566           *last = '\\';
567           break;
568         }
569         case '/':
570         {
571           *last = '/';
572           break;
573         }
574         case 'b':
575         {
576           *last = '\b';
577           break;
578         }
579         case 'f':
580         {
581           *last = '\f';
582           break;
583         }
584         case 'n':
585         {
586           *last = '\n';
587           break;
588         }
589         case 'r':
590         {
591           *last = '\r';
592           break;
593         }
594         case 't':
595         {
596           *last = '\t';
597           break;
598         }
599         case 'u':
600         {
601           unsigned int codepoint;
602           if( !AtLeast(6) )
603           {
604             static_cast<void>( Error("Bad unicode codepoint; not enough characters") );
605             return nullptr;
606           }
607           if ( !HexStringToUnsignedInteger(&(*(mIter + 2)), &(*(mIter + 6)), codepoint) )
608           {
609             static_cast<void>( Error("Bad unicode codepoint") );
610             return nullptr;
611           }
612
613           if (codepoint <= 0x7F)
614           {
615             *last = (char)codepoint;
616           }
617           else if (codepoint <= 0x7FF)
618           {
619             *last++ = (char)(0xC0 | (codepoint >> 6));
620             *last = (char)(0x80 | (codepoint & 0x3F));
621           }
622           else if (codepoint <= 0xFFFF)
623           {
624             *last++ = (char)(0xE0 | (codepoint >> 12));
625             *last++ = (char)(0x80 | ((codepoint >> 6) & 0x3F));
626             *last = (char)(0x80 | (codepoint & 0x3F));
627           }
628
629           Advance(4);
630           break;
631         } // case 'u' unicode
632
633         default:
634         {
635           static_cast<void>( Error("Unrecognized escape sequence") );
636           return nullptr;
637         }
638       }
639
640       ++last;
641       Advance(2);
642     }
643     else if (*mIter == '{')
644     {
645       if((0 == substitution) && (*last != '\\'))
646       {
647         substitution = 1;
648       }
649       *last++ = *mIter;
650       Advance(1);
651     }
652     else if (*mIter == '}')
653     {
654       if(substitution)
655       {
656         substitution++;
657       }
658       *last++ = *mIter;
659       Advance(1);
660     }
661     else if (*mIter == '"')
662     {
663       *last = 0;
664       Advance(1);
665       break;
666     }
667     else
668     {
669       *last++ = *mIter;
670       Advance(1);
671     }
672
673   } // while(*mIter)
674
675   mNumberOfParsedChars += last - first;
676   mNumberOfParsedChars += 1 ; // null terminator
677
678   mCurrent.SetSubstitution( substitution > 1 );
679
680   // return true;
681   return &(*first);
682
683 } // ParseString()
684
685 bool JsonParserState::HandleStartState(const char* name, const char currentChar)
686 {
687   if( '{' == currentChar )
688   {
689     NewNode(name, TreeNode::OBJECT);
690     mState = STATE_OBJECT;
691   }
692   else if( '[' == currentChar )
693   {
694     NewNode(name, TreeNode::ARRAY);
695     mState = STATE_VALUE;
696   }
697   else
698   {
699     return Error("Json must start with object {} or array []");
700   }
701
702   AdvanceSkipWhiteSpace(1);
703   return true;
704 }
705
706 bool JsonParserState::HandleObjectState(const char currentChar, const char lastCharacter)
707 {
708   if( '}' == currentChar )
709   {
710     if(',' == lastCharacter)
711     {
712       return Error("Unexpected comma");
713     }
714
715     if( !UpToParent() )
716     {
717       return false;
718     }
719     mState = STATE_VALUE;
720   }
721   else if ( '"' == currentChar )
722   {
723     mState = STATE_KEY;
724   }
725   else
726   {
727     return Error("Unexpected character");
728   }
729
730   AdvanceSkipWhiteSpace(1);
731   return true;
732 }
733
734 bool JsonParserState::HandleKeyState(char*& name)
735 {
736   name = EncodeString();
737   if( nullptr == name )
738   {
739     return false;
740   }
741   if( !ParseWhiteSpace() )
742   {
743     return false;
744   }
745   if( ':' != Char())
746   {
747     return Error("Expected ':'");
748   }
749   if( !ParseWhiteSpace() )
750   {
751     return false;
752   }
753   mState = STATE_VALUE;
754
755   AdvanceSkipWhiteSpace(1);
756   return true;
757 }
758
759 bool JsonParserState::HandleCharacterQuote(char*& name)
760 {
761   Advance(1);
762   NewNode(name, TreeNode::STRING);
763   if( char* value = EncodeString() )
764   {
765     mCurrent.SetString(value);
766   }
767   else
768   {
769     return false;
770   }
771   if( !UpToParent() )
772   {
773     return false;
774   }
775   AdvanceSkipWhiteSpace(0);
776   return true;
777 }
778
779 bool JsonParserState::HandleCharacterNumberOrHyphen(const char* name)
780 {
781   NewNode(name, TreeNode::IS_NULL);
782   if( !ParseNumber() )
783   {
784     return false;
785   }
786   if( !UpToParent() )
787   {
788     return false;
789   }
790   AdvanceSkipWhiteSpace(0);
791   return true;
792 }
793
794 bool JsonParserState::HandleValueState(char*& name, const char currentChar, const char lastCharacter)
795 {
796   bool handled = true;
797
798   if( '"' == currentChar )
799   {
800     handled = HandleCharacterQuote(name);
801   }
802   else if( IsNumber(currentChar) || currentChar == '-' )
803   {
804     handled = HandleCharacterNumberOrHyphen(name);
805   }
806   else if( '{' == currentChar )
807   {
808     handled = HandleCharacterBracesStart(name, lastCharacter);
809   }
810   else if( '}' == currentChar )
811   {
812     handled = HandleCharacterBracesEnd(lastCharacter);
813   }
814   else if( '[' == currentChar )
815   {
816     handled = HandleCharacterSquareBracketStart(name);
817   }
818   else if( ']' == currentChar )
819   {
820     handled = HandleCharacterSquareBracketEnd(lastCharacter);
821   }
822   else if( 't' == currentChar )
823   {
824     handled = HandleCharacterLowercaseT(name);
825   }
826   else if( 'n' == currentChar )
827   {
828     handled = HandleCharacterLowercaseN(name);
829   }
830   else if( 'f' == currentChar)
831   {
832     handled = HandleCharacterLowercaseF(name);
833   }
834   else if( ',' == currentChar )
835   {
836     handled = HandleCharacterComma(name);
837   }
838   else
839   {
840     handled = Error("Unexpected character");
841   }
842
843   if(handled)
844   {
845     name = nullptr;
846   }
847
848   return handled;
849 }
850
851 bool JsonParserState::ParseJson(VectorChar& source)
852 {
853   Reset();
854
855   if( 0 == source.size() )
856   {
857     return Error("Empty source buffer to parse");
858   }
859
860   mIter = source.begin();
861   mEnd  = source.end();
862
863   char* name = nullptr;
864   char currentChar   = 0;
865   char lastCharacter = 0;
866
867   if( !ParseWhiteSpace() )
868   {
869     return false;
870   }
871
872   while(mIter != mEnd)
873   {
874     lastCharacter = currentChar;
875     currentChar = Char();
876
877     switch( mState )
878     {
879       case STATE_START:
880       {
881         if(!HandleStartState(name, currentChar))
882         {
883           return false;
884         }
885         break;
886       }
887       case STATE_OBJECT:
888       {
889         if(!HandleObjectState(currentChar, lastCharacter))
890         {
891           return false;
892         }
893         break;
894       }
895       case STATE_KEY:
896       {
897         if(!HandleKeyState(name))
898         {
899           return false;
900         }
901         break;
902       }
903       case STATE_VALUE:
904       {
905         if(!HandleValueState(name, currentChar, lastCharacter))
906         {
907           return false;
908         }
909         break;
910       } // case STATE_VALUE
911       case STATE_END:
912       {
913         return Error("Unexpected character. Json must have one object or array at its root");
914         break;
915       }
916     } // switch(mState)
917
918   } // while(1)
919
920   //
921   if( mState != STATE_END )
922   {
923     return Error("Unexpected termination character");
924   }
925
926   mIter = source.end();
927
928   return true;
929
930 } // ParseJson
931
932 void JsonParserState::Reset()
933 {
934   mCurrent = TreeNodeManipulator(mRoot);
935
936   mErrorDescription   = nullptr;
937   mErrorNewLine       = 0;
938   mErrorColumn        = 0;
939   mErrorPosition      = 0;
940 }
941
942 bool JsonParserState::HandleCharacterBracesStart(const char* name, const char lastCharacter)
943 {
944   if( '}' == lastCharacter )
945   {
946     return Error("Expected a comma");
947   }
948   else
949   {
950     NewNode(name, TreeNode::OBJECT);
951     mState = STATE_OBJECT;
952     AdvanceSkipWhiteSpace(1);
953   }
954   return true;
955 }
956
957 bool JsonParserState::HandleCharacterBracesEnd(const char lastCharacter)
958 {
959   if(',' == lastCharacter)
960   {
961     return Error("Expected another value");
962   }
963
964   if(mCurrent.GetType() != TreeNode::OBJECT)
965   {
966     return Error("Mismatched array definition");
967   }
968
969   if(mCurrent.GetParent() == nullptr)
970   {
971     mState = STATE_END;
972   }
973   else
974   {
975     if( !UpToParent() )
976     {
977       return false;
978     }
979   }
980   AdvanceSkipWhiteSpace(1);
981   return true;
982 }
983
984 bool JsonParserState::HandleCharacterSquareBracketStart(const char* name)
985 {
986   NewNode(name, TreeNode::ARRAY);
987   mState = STATE_VALUE;
988   AdvanceSkipWhiteSpace(1);
989   return true;
990 }
991
992 bool JsonParserState::HandleCharacterSquareBracketEnd(const char lastCharacter)
993 {
994   if(',' == lastCharacter)
995   {
996     return Error("Expected a value");
997   }
998
999   if(mCurrent.GetType() != TreeNode::ARRAY)
1000   {
1001     return Error("Mismatched braces in object definition");
1002   }
1003
1004   if(mCurrent.GetParent() == nullptr)
1005   {
1006     mState = STATE_END;
1007   }
1008   else
1009   {
1010     if( !UpToParent() )
1011     {
1012       return false;
1013     }
1014   }
1015   AdvanceSkipWhiteSpace(1);
1016   return true;
1017 }
1018
1019 bool JsonParserState::HandleCharacterLowercaseT(const char* name)
1020 {
1021   NewNode(name, TreeNode::BOOLEAN);
1022   if( !ParseTrue() )
1023   {
1024     return false;
1025   }
1026   if( !UpToParent() )
1027   {
1028     return false;
1029   }
1030   AdvanceSkipWhiteSpace(0);
1031   return true;
1032 }
1033
1034 bool JsonParserState::HandleCharacterLowercaseN(const char* name)
1035 {
1036   NewNode(name, TreeNode::IS_NULL);
1037   if( !ParseNULL() )
1038   {
1039     return false;
1040   }
1041   if( !UpToParent() )
1042   {
1043     return false;
1044   }
1045   AdvanceSkipWhiteSpace(0);
1046   return true;
1047 }
1048
1049 bool JsonParserState::HandleCharacterLowercaseF(const char* name)
1050 {
1051   NewNode(name, TreeNode::BOOLEAN);
1052   if( !ParseFalse() )
1053   {
1054     return false;
1055   }
1056   if( !UpToParent() )
1057   {
1058     return false;
1059   }
1060   AdvanceSkipWhiteSpace(0);
1061   return true;
1062 }
1063
1064 bool JsonParserState::HandleCharacterComma(const char* name)
1065 {
1066   if( 0 == mCurrent.Size() )
1067   {
1068     return Error("Missing Value");
1069   }
1070
1071   if(mCurrent.GetType() == TreeNode::OBJECT)
1072   {
1073     mState = STATE_OBJECT; // to get '"' in '"key":val'
1074   }
1075   else if(mCurrent.GetType() == TreeNode::ARRAY)
1076   {
1077     mState = STATE_VALUE; // array so just get next value
1078   }
1079   else
1080   {
1081     return Error("Unexpected character");
1082   }
1083   AdvanceSkipWhiteSpace(1);
1084   return true;
1085 }
1086
1087 } // namespace Internal
1088
1089 } // namespace Toolkit
1090
1091 } // namespace Dali